diff --git a/android/app/build.gradle b/android/app/build.gradle index d76c2b16f587..7f8a1c615078 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001047104 - versionName "1.4.71-4" + versionCode 1001047201 + versionName "1.4.72-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" diff --git a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md b/contributingGuides/PROPTYPES_CONVERSION_TABLE.md deleted file mode 100644 index 7e1c1dda4262..000000000000 --- a/contributingGuides/PROPTYPES_CONVERSION_TABLE.md +++ /dev/null @@ -1,144 +0,0 @@ -# Expensify PropTypes Conversion Table - -This is a reference to help you convert PropTypes to TypeScript types. - -## Table of Contents - -- [Important Considerations](#important-considerations) - - [Don't Rely on `isRequired`](#dont-rely-on-isrequired) -- [PropTypes Conversion Table](#proptypes-conversion-table) -- [Conversion Example](#conversion-example) - -## Important Considerations - -### Don't Rely on `isRequired` - -Regardless of `isRequired` is present or not on props in `PropTypes`, read through the component implementation to check if props without `isRequired` can actually be optional. The use of `isRequired` is not consistent in the current codebase. Just because `isRequired` is not present, it does not necessarily mean that the prop is optional. - -One trick is to mark the prop in question with optional modifier `?`. See if the "possibly `undefined`" error is raised by TypeScript. If any error is raised, the implementation assumes the prop not to be optional. - -```ts -// Before -const propTypes = { - isVisible: PropTypes.bool.isRequired, - // `confirmText` prop is not marked as required here, theoretically it is optional. - confirmText: PropTypes.string, -}; - -// After -type ComponentProps = { - isVisible: boolean; - // Consider it as required unless you have proof that it is indeed an optional prop. - confirmText: string; // vs. confirmText?: string; -}; -``` - -## PropTypes Conversion Table - -| PropTypes | TypeScript | Instructions | -| -------------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PropTypes.any` | `T`, `Record` or `unknown` | Figure out what would be the correct data type and use it.

If you know that it's a object but isn't possible to determine the internal structure, use `Record`. | -| `PropTypes.array` or `PropTypes.arrayOf(T)` | `T[]` or `Array` | Convert to `T[]`, where `T` is the data type of the array.

If `T` isn't a primitive type, create a separate `type` for the object structure of your prop and use it. | -| `PropTypes.bool` | `boolean` | Convert to `boolean`. | -| `PropTypes.func` | `(arg1: Type1, arg2: Type2...) => ReturnType` | Convert to the function signature. | -| `PropTypes.number` | `number` | Convert to `number`. | -| `PropTypes.object`, `PropTypes.shape(T)` or `PropTypes.exact(T)` | `T` | If `T` isn't a primitive type, create a separate `type` for the `T` object structure of your prop and use it.

If you want an object but it isn't possible to determine the internal structure, use `Record`. | -| `PropTypes.objectOf(T)` | `Record` | Convert to a `Record` where `T` is the data type of values stored in the object.

If `T` isn't a primitive type, create a separate `type` for the object structure and use it. | -| `PropTypes.string` | `string` | Convert to `string`. | -| `PropTypes.node` | `React.ReactNode` | Convert to `React.ReactNode`. `ReactNode` includes `ReactElement` as well as other types such as `strings`, `numbers`, `arrays` of the same, `null`, and `undefined` In other words, anything that can be rendered in React is a `ReactNode`. | -| `PropTypes.element` | `React.ReactElement` | Convert to `React.ReactElement`. | -| `PropTypes.symbol` | `symbol` | Convert to `symbol`. | -| `PropTypes.elementType` | `React.ElementType` | Convert to `React.ElementType`. | -| `PropTypes.instanceOf(T)` | `T` | Convert to `T`. | -| `PropTypes.oneOf([T, U, ...])` or `PropTypes.oneOfType([T, U, ...])` | `T \| U \| ...` | Convert to a union type e.g. `T \| U \| ...`. | - -## Conversion Example - -```ts -// Before -const propTypes = { - unknownData: PropTypes.any, - anotherUnknownData: PropTypes.any, - indexes: PropTypes.arrayOf(PropTypes.number), - items: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string, - label: PropTypes.string, - }) - ), - shouldShowIcon: PropTypes.bool, - onChangeText: PropTypes.func, - count: PropTypes.number, - session: PropTypes.shape({ - authToken: PropTypes.string, - accountID: PropTypes.number, - }), - errors: PropTypes.objectOf(PropTypes.string), - inputs: PropTypes.objectOf( - PropTypes.shape({ - id: PropTypes.string, - label: PropTypes.string, - }) - ), - label: PropTypes.string, - anchor: PropTypes.node, - footer: PropTypes.element, - uniqSymbol: PropTypes.symbol, - icon: PropTypes.elementType, - date: PropTypes.instanceOf(Date), - size: PropTypes.oneOf(["small", "medium", "large"]), - - optionalString: PropTypes.string, - /** - * Note that all props listed above are technically optional because they lack the `isRequired` attribute. - * However, in most cases, props are actually required but the `isRequired` attribute is left out by mistake. - * - * For each prop that appears to be optional, determine whether the component implementation assumes that - * the prop has a value (making it non-optional) or not. Only those props that are truly optional should be - * labeled with a `?` in their type definition. - */ -}; - -// After -type Item = { - value: string; - label: string; -}; - -type Session = { - authToken: string; - accountID: number; -}; - -type Input = { - id: string; - label: string; -}; - -type Size = "small" | "medium" | "large"; - -type ComponentProps = { - unknownData: string[]; - - // It's not possible to infer the data as it can be anything because of reasons X, Y and Z. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - anotherUnknownData: unknown; - - indexes: number[]; - items: Item[]; - shouldShowIcon: boolean; - onChangeText: (value: string) => void; - count: number; - session: Session; - errors: Record; - inputs: Record; - label: string; - anchor: React.ReactNode; - footer: React.ReactElement; - uniqSymbol: symbol; - icon: React.ElementType; - date: Date; - size: Size; - optionalString?: string; -}; -``` diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 36815cd0557c..22b1dea61bae 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -1,5 +1,63 @@ -# JavaScript Coding Standards - +# Coding Standards + +## Table of Contents + +- [Introduction](#introduction) +- [TypeScript guidelines](#typescript-guidelines) + - [General rules](#general-rules) + - [`d.ts` extension](#dts-extension) + - [Type Alias vs Interface](#type-alias-vs-interface) + - [Enum vs. Union Type](#enum-vs-union-type) + - [`unknown` vs. `any`](#unknown-vs-any) + - [`T[]` vs. `Array`](#t-vs-arrayt) + - [`@ts-ignore`](#ts-ignore) + - [Type Inference](#type-inference) + - [Utility Types](#utility-types) + - [`object` type](#object-type) + - [Prop Types](#prop-types) + - [File organization](#file-organization) + - [Reusable Types](#reusable-types) + - [`tsx` extension](#tsx-extension) + - [No inline prop types](#no-inline-prop-types) + - [Satisfies Operator](#satisfies-operator) + - [Type imports/exports](#type-importsexports) + - [Refs](#refs) + - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) +- [Naming Conventions](#naming-conventions) + - [Type names](#type-names) + - [Prop callbacks](#prop-callbacks) + - [Event Handlers](#event-handlers) + - [Boolean variables and props](#boolean-variables-and-props) + - [Functions](#functions) + - [`var`, `const` and `let`](#var-const-and-let) +- [Object / Array Methods](#object--array-methods) +- [Accessing Object Properties and Default Values](#accessing-object-properties-and-default-values) +- [JSDocs](#jsdocs) +- [Component props](#component-props) +- [Destructuring](#destructuring) +- [Named vs Default Exports in ES6 - When to use what?](#named-vs-default-exports-in-es6---when-to-use-what) +- [Classes and constructors](#classes-and-constructors) + - [Class syntax](#class-syntax) + - [Constructor](#constructor) +- [ESNext: Are we allowed to use [insert new language feature]? Why or why not?](#esnext-are-we-allowed-to-use-insert-new-language-feature-why-or-why-not) +- [React Coding Standards](#react-coding-standards) + - [Code Documentation](#code-documentation) + - [Inline Ternaries](#inline-ternaries) + - [Function component style](#function-component-style) + - [Forwarding refs](#forwarding-refs) + - [Hooks and HOCs](#hooks-and-hocs) + - [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) + - [Composition](#composition) + - [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) +- [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions) +- [Onyx Best Practices](#onyx-best-practices) + - [Collection Keys](#collection-keys) +- [Learning Resources](#learning-resources) + +## Introduction + + For almost all of our code style rules, refer to the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript). When writing ES6 or React code, please also refer to the [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/tree/master/react). @@ -10,13 +68,508 @@ We use Prettier to automatically style our code. There are a few things that we have customized for our tastes which will take precedence over Airbnb's guide. +## TypeScript guidelines + +### General rules + +Strive to type as strictly as possible. + +```ts +type Foo = { + fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; + person: { name: string; age: number }; // vs. person: Record; +}; +``` + +### `d.ts` Extension + +Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using [module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation). + +> Why? Type errors in `d.ts` files are not checked by TypeScript. + +### Type Alias vs. Interface + +Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) + +> Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. + +```ts +// BAD +interface Person { + name: string; +} + +// GOOD +type Person = { + name: string; +}; +``` + +### Enum vs. Union Type + +Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) + +> Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. + +```ts +// Most simple form of union type. +type Color = "red" | "green" | "blue"; +function printColors(color: Color) { + console.log(color); +} + +// When the values need to be iterated upon. +import { TupleToUnion } from "type-fest"; + +const COLORS = ["red", "green", "blue"] as const; +type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' + +for (const color of COLORS) { + printColor(color); +} + +// When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) +import { ValueOf } from "type-fest"; + +const COLORS = { + Red: "red", + Green: "green", + Blue: "blue", +} as const; +type Color = ValueOf; // type: 'red' | 'green' | 'blue' + +printColor(COLORS.Red); +``` + +### `unknown` vs. `any` + +Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) + +> Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. + +```ts +const value: unknown = JSON.parse(someJson); +if (typeof value === 'string') {...} +else if (isPerson(value)) {...} +... +``` + +### `T[]` vs. `Array` + +Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) + +```ts +// Array +const a: Array = ["a", "b"]; +const b: Array<{ prop: string }> = [{ prop: "a" }]; +const c: Array<() => void> = [() => {}]; + +// T[] +const d: MyType[] = ["a", "b"]; +const e: string[] = ["a", "b"]; +const f: readonly string[] = ["a", "b"]; +``` + +### `@ts-ignore` + +Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. + +### Type Inference + +When possible, allow the compiler to infer type of variables. + +```ts +// BAD +const foo: string = "foo"; +const [counter, setCounter] = useState(0); + +// GOOD +const foo = "foo"; +const [counter, setCounter] = useState(0); +const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be inferred from the default value of undefined +``` + +For function return types, default to always typing them unless a function is simple enough to reason about its return type. + +> Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. + +```ts +function simpleFunction(name: string) { + return `hello, ${name}`; +} + +function complicatedFunction(name: string): boolean { +// ... some complex logic here ... + return foo; +} +``` + +### Utility Types + +Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. + +```ts +type Foo = { + bar: string; +}; + +// BAD +type ReadOnlyFoo = { + readonly [Property in keyof Foo]: Foo[Property]; +}; + +// GOOD +type ReadOnlyFoo = Readonly; + +// BAD +type FooValue = Foo[keyof Foo]; + +// GOOD +type FooValue = ValueOf; + +``` + +### `object` type + +Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) + +> Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. + +```ts +// BAD +const foo: object = [1, 2, 3]; // TypeScript does not error +``` + +If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. + +> Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. + +```ts +function logObject(object: Record) { + for (const [key, value] of Object.entries(object)) { + console.log(`${key}: ${value}`); + } +} +``` + +### Prop Types + +Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. + +> Why? Importing prop type from the component file is more common and readable. Using `ComponentProps` might cause problems in some cases (see [related GitHub issue](https://github.com/piotrwitek/react-redux-typescript-guide/issues/170)). Each component with props has it's prop type defined in the file anyway, so it's easy to export it when required. + +Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. + +```tsx +// MyComponent.tsx +export type MyComponentProps = { + foo: string; +}; + +export default function MyComponent({ foo }: MyComponentProps) { + return {foo}; +} + +// BAD +import { ComponentProps } from "React"; +import MyComponent from "./MyComponent"; +type MyComponentProps = ComponentProps; + +// GOOD +import MyComponent, { MyComponentProps } from "./MyComponent"; +``` + +### File organization + +In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. + +> Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implementation (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. + +Utility module example + +```ts +// types.ts +type GreetingModule = { + getHello: () => string; + getGoodbye: () => string; +}; + +// index.native.ts +import { GreetingModule } from "./types"; +function getHello() { + return "hello from mobile code"; +} +function getGoodbye() { + return "goodbye from mobile code"; +} +const Greeting: GreetingModule = { + getHello, + getGoodbye, +}; +export default Greeting; + +// index.ts +import { GreetingModule } from "./types"; +function getHello() { + return "hello from other platform code"; +} +function getGoodbye() { + return "goodbye from other platform code"; +} +const Greeting: GreetingModule = { + getHello, + getGoodbye, +}; +export default Greeting; +``` + +Component module example + +```ts +// types.ts +export type MyComponentProps = { + foo: string; +} + +// index.ios.ts +import { MyComponentProps } from "./types"; + +export MyComponentProps; +export default function MyComponent({ foo }: MyComponentProps) { /* ios specific implementation */ } + +// index.ts +import { MyComponentProps } from "./types"; + +export MyComponentProps; +export default function MyComponent({ foo }: MyComponentProps) { /* Default implementation */ } +``` + +### Reusable Types + +Reusable type definitions, such as models (e.g., Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. + +```ts +// src/types/Report.ts + +type Report = {...}; + +export default Report; +``` + +### `tsx` extension + +Use `.tsx` extension for files that contain React syntax. + +> Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. + +### No inline prop types + +Do not define prop types inline for components that are exported. + +> Why? Prop types might [need to be exported from component files](#export-prop-types). If the component is only used inside a file or module and not exported, then inline prop types can be used. + +```ts +// BAD +export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ + // component implementation +}; + +// GOOD +type MyComponentProps = { foo: string, bar: number }; +export default MyComponent({ foo, bar }: MyComponentProps){ + // component implementation +} +``` + +### Satisfies Operator + +Use the `satisfies` operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression. + +> Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The `satisfies` operator assists in doing both. + +```ts +// BAD +const sizingStyles = { + w50: { + width: '50%', + }, + mw100: { + maxWidth: '100%', + }, +} as const; + +// GOOD +const sizingStyles = { + w50: { + width: '50%', + }, + mw100: { + maxWidth: '100%', + }, +} as const satisfies Record; +``` + +The example above results in the most narrow type possible, also the values are `readonly`. There are some cases in which that is not desired (e.g. the variable can be modified), if so `as const` should be omitted. + +### Type imports/exports + +Always use the `type` keyword when importing/exporting types + +> Why? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the `type` keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundle + +Imports: +```ts +// BAD +import {SomeType} from './a' +import someVariable from './a' + +import {someVariable, SomeOtherType} from './b' + +// GOOD +import type {SomeType} from './a' +import someVariable from './a' +``` + + Exports: +```ts +// BAD +export {SomeType} +export someVariable +// or +export {someVariable, SomeOtherType} + +// GOOD +export type {SomeType} +export someVariable +``` + +### Refs + +Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components with [Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointer and Mouse events. Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component assert it as soon as possible using utility methods declared in `src/types/utils`. + +Normal usage: +```tsx +const ref = useRef(); + + {#DO SOMETHING}}> +``` + +Exceptional usage where DOM methods are necessary: +```tsx +import viewRef from '@src/types/utils/viewRef'; + +const ref = useRef(); + +if (ref.current && 'getBoundingClientRect' in ref.current) { + ref.current.getBoundingClientRect(); +} + + {#DO SOMETHING}}> +``` + +### Other Expensify Resources on TypeScript + +- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) + ## Naming Conventions +### Type names + + - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) + + ```ts + // BAD + type foo = ...; + type BAR = ...; + + // GOOD + type Foo = ...; + type Bar = ...; + ``` + + - Do not postfix type aliases with `Type`. + + ```ts + // BAD + type PersonType = ...; + + // GOOD + type Person = ...; + ``` + + - Use singular name for union types. + + ```ts + // BAD + type Colors = "red" | "blue" | "green"; + + // GOOD + type Color = "red" | "blue" | "green"; + ``` + + - Use `{ComponentName}Props` pattern for prop types. + + ```ts + // BAD + type Props = { + // component's props + }; + + function MyComponent({}: Props) { + // component's code + } + + // GOOD + type MyComponentProps = { + // component's props + }; + + function MyComponent({}: MyComponentProps) { + // component's code + } + ``` + + - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. + + > Prefix each type parameter name to distinguish them from other types. + + ```ts + // BAD + type KeyValuePair = { key: K; value: U }; + + type Keys = Array; + + // GOOD + type KeyValuePair = { key: TKey; value: TValue }; + + type Keys = Array; + type Keys = Array; + ``` + +### Prop callbacks + - Prop callbacks should be named for what has happened, not for what is going to happen. Components should never assume anything about how they will be used (that's the job of whatever is implementing it). + + ```ts + // Bad + type ComponentProps = { + /** A callback to call when we want to save the form */ + onSaveForm: () => void; + }; + + // Good + type ComponentProps = { + /** A callback to call when the form has been submitted */ + onFormSubmitted: () => void; + }; + ``` + + * Do not use underscores when naming private methods. + ### Event Handlers - When you have an event handler, do not prefix it with "on" or "handle". The method should be named for what it does, not what it handles. This promotes code reuse by minimizing assumptions that a method must be called in a certain fashion (eg. only as an event handler). - One exception for allowing the prefix of "on" is when it is used for callback `props` of a React component. Using it in this way helps to distinguish callbacks from public component methods. - ```javascript + ```ts // Bad const onSubmitClick = () => { // Validate form items and submit form @@ -32,7 +585,7 @@ There are a few things that we have customized for our tastes which will take pr - Boolean props or variables must be prefixed with `should` or `is` to make it clear that they are `Boolean`. Use `should` when we are enabling or disabling some features and `is` in most other cases. -```javascript +```tsx // Bad @@ -46,11 +599,11 @@ const valid = props.something && props.somethingElse; const isValid = props.something && props.somethingElse; ``` -## Functions +### Functions Any function declared in a library module should use the `function myFunction` keyword rather than `const myFunction`. -```javascript +```tsx // Bad const myFunction = () => {...}; @@ -68,23 +621,9 @@ export { } ``` -Using named functions is the preferred way to write a callback method. - -```javascript -// Bad -people.map(function (item) {/* Long and complex logic */}); -people.map((item) => {/* Long and complex logic with many inner loops*/}); -useEffect/useMemo/useCallback(() => {/* Long and complex logic */}, []); - -// Good -function mappingPeople(person) {/* Long and complex logic */}; -people.map(mappingPeople); -useEffect/useMemo/useCallback(function handlingConnection() {/* Long and complex logic */}, []); -``` - You can still use arrow function for declarations or simple logics to keep them readable. -```javascript +```tsx // Bad randomList.push({ onSelected: Utils.checkIfAllowed(function checkTask() { return Utils.canTeamUp(people); }), @@ -112,17 +651,6 @@ useEffect(() => { ``` -Empty functions (noop) should be declare as arrow functions with no whitespace inside. Avoid _.noop() - -```javascript -// Bad -const callback = _.noop; -const callback = () => { }; - -// Good -const callback = () => {}; -``` - ## `var`, `const` and `let` - Never use `var` @@ -130,7 +658,7 @@ const callback = () => {}; - Try to write your code in a way where the variable reassignment isn't necessary - Use `let` only if there are no other options -```javascript +```tsx // Bad let array = []; @@ -148,82 +676,97 @@ if (someCondition) { ## Object / Array Methods -We have standardized on using [underscore.js](https://underscorejs.org/) methods for objects and collections instead of the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods). This is mostly to maintain consistency, but there are some type safety features and conveniences that underscore methods provide us e.g. the ability to iterate over an object and the lack of a `TypeError` thrown if a variable is `undefined`. +We have standardized on using the native [Array instance methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array#instance_methods) instead of [lodash](https://lodash.com/) methods for objects and collections. As the vast majority of code is written in TypeScript, we can safely use the native methods. -```javascript +```ts // Bad -myArray.forEach(item => doSomething(item)); -// Good _.each(myArray, item => doSomething(item)); +// Good +myArray.forEach(item => doSomething(item)); // Bad -const myArray = Object.keys(someObject).map((key) => doSomething(someObject[key])); -// Good const myArray = _.map(someObject, (value, key) => doSomething(value)); +// Good +const myArray = Object.keys(someObject).map((key) => doSomething(someObject[key])); // Bad -myCollection.includes('item'); -// Good _.contains(myCollection, 'item'); +// Good +myCollection.includes('item'); // Bad -const modifiedArray = someArray.filter(filterFunc).map(mapFunc); -// Good const modifiedArray = _.chain(someArray) .filter(filterFunc) .map(mapFunc) .value(); +// Good +const modifiedArray = someArray.filter(filterFunc).map(mapFunc); ``` ## Accessing Object Properties and Default Values -Use `lodashGet()` to safely access object properties and `||` to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. In the rare case that you want to consider a falsy value as usable and the `||` operator prevents this, then be explicit about this in your code and check for the type. +Use optional chaining (`?.`) to safely access object properties and nullish coalescing (`??`) to short circuit null or undefined values that are not guaranteed to exist in a consistent way throughout the codebase. Don't use the `lodashGet()` function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) -```javascript -// Bad -const value = somePossiblyNullThing ?? 'default'; -// Good -const value = somePossiblyNullThing || 'default'; -// Bad -const value = someObject.possiblyUndefinedProperty?.nestedProperty || 'default'; -// Bad -const value = (someObject && someObject.possiblyUndefinedProperty && someObject.possiblyUndefinedProperty.nestedProperty) || 'default'; -// Good -const value = lodashGet(someObject, 'possiblyUndefinedProperty.nestedProperty', 'default'); +```ts +// BAD +import lodashGet from "lodash/get"; +const name = lodashGet(user, "name", "default name"); + +// BAD +const name = user?.name || "default name"; + +// GOOD +const name = user?.name ?? "default name"; ``` ## JSDocs -- Always document parameters and return values. -- Optional parameters should be enclosed by `[]` e.g. `@param {String} [optionalText]`. -- Document object parameters with separate lines e.g. `@param {Object} parameters` followed by `@param {String} parameters.field`. -- If a parameter accepts more than one type, use `*` to denote there is no single type. -- Use uppercase when referring to JS primitive values (e.g. `Boolean` not `bool`, `Number` not `int`, etc). -- When specifying a return value use `@returns` instead of `@return`. If there is no return value, do not include one in the doc. - -- Avoid descriptions that don't add any additional information. Method descriptions should only be added when its behavior is unclear. +- Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) +- Only document params/return values if their names are not enough to fully understand their purpose. Not all parameters or return values need to be listed in the JSDoc comment. If there is no comment accompanying the parameter or return value, omit it. +- When specifying a return value use `@returns` instead of `@return`. +- Avoid descriptions that don't add any additional information. Method descriptions should only be added when it's behavior is unclear. - Do not use block tags other than `@param` and `@returns` (e.g. `@memberof`, `@constructor`, etc). - Do not document default parameters. They are already documented by adding them to a declared function's arguments. - Do not use record types e.g. `{Object.}`. -- Do not create `@typedef` to use in JSDocs. -- Do not use type unions e.g. `{(number|boolean)}`. -```javascript -// Bad +```ts +// BAD /** - * Populates the shortcut modal - * @param {bool} shouldShowAdvancedShortcuts whether to show advanced shortcuts - * @return {*} + * @param {number} age + * @returns {boolean} Whether the person is a legal drinking age or nots */ -function populateShortcutModal(shouldShowAdvancedShortcuts) { +function canDrink(age: number): boolean { + return age >= 21; } -// Good +// GOOD /** - * @param {Boolean} shouldShowAdvancedShortcuts - * @returns {Boolean} + * @returns Whether the person is a legal drinking age or nots */ -function populateShortcutModal(shouldShowAdvancedShortcuts) { +function canDrink(age: number): boolean { + return age >= 21; +} +``` + +In the above example, because the parameter `age` doesn't have any accompanying comment, it is completely omitted from the JSDoc. + +## Component props + +Do not use **`propTypes` and `defaultProps`**: . Use object destructing and assign a default value to each optional prop unless the default values is `undefined`. + +```tsx +type MyComponentProps = { + requiredProp: string; + optionalPropWithDefaultValue?: number; + optionalProp?: boolean; +}; + +function MyComponent({ + requiredProp, + optionalPropWithDefaultValue = 42, + optionalProp, +}: MyComponentProps) { + // component's code } ``` @@ -234,7 +777,7 @@ We should avoid using object destructuring in situations where it reduces code c - Avoid object destructuring for a single variable that you only use *once*. It's clearer to use dot notation for accessing a single variable. -```javascript +```ts // Bad const {data} = event.data; @@ -242,36 +785,6 @@ const {data} = event.data; const {name, accountID, email} = data; ``` -**React Components** - -Always use destructuring to get prop values. Destructuring is necessary to assign default values to props. - -```javascript -// Bad -function UserInfo(props) { - return ( - - Name: {props.name} - Email: {props.email} - - ); -} - -UserInfo.defaultProps = { - name: 'anonymous'; -} - -// Good -function UserInfo({ name = 'anonymous', email }) { - return ( - - Name: {name} - Email: {email} - - ); -} -``` - ## Named vs Default Exports in ES6 - When to use what? ES6 provides two ways to export a module from a file: `named export` and `default export`. Which variation to use depends on how the module will be used. @@ -283,7 +796,7 @@ ES6 provides two ways to export a module from a file: `named export` and `defaul - All exports (both default and named) should happen at the bottom of the file - Do **not** export individual features inline. -```javascript +```ts // Bad export const something = 'nope'; export const somethingElse = 'stop'; @@ -300,10 +813,10 @@ export { ## Classes and constructors -#### Class syntax +### Class syntax Using the `class` syntax is preferred wherever appropriate. Airbnb has clear [guidelines](https://github.com/airbnb/javascript#classes--constructors) in their JS style guide which promotes using the _class_ syntax. Don't manipulate the `prototype` directly. The `class` syntax is generally considered more concise and easier to understand. -#### Constructor +### Constructor Classes have a default constructor if one is not specified. No need to write a constructor function that is empty or just delegates to a parent class. ```js @@ -341,105 +854,50 @@ So, if a new language feature isn't something we have agreed to support it's off Here are a couple of things we would ask that you *avoid* to help maintain consistency in our codebase: - **Async/Await** - Use the native `Promise` instead -- **Optional Chaining** - Use `lodashGet()` to fetch a nested value instead -- **Null Coalescing Operator** - Use `lodashGet()` or `||` to set a default value for a possibly `undefined` or `null` variable -# React Coding Standards +## React Coding Standards -# React specific styles +### Code Documentation -## Method Naming and Code Documentation -* Prop callbacks should be named for what has happened, not for what is going to happen. Components should never assume anything about how they will be used (that's the job of whatever is implementing it). +* Add descriptions to all component props using a block comment above the definition. No need to document the types, but add some context for each property so that other developers understand the intended use. -```javascript +```tsx // Bad -const propTypes = { - /** A callback to call when we want to save the form */ - onSaveForm: PropTypes.func.isRequired, -}; - -// Good -const propTypes = { - /** A callback to call when the form has been submitted */ - onFormSubmitted: PropTypes.func.isRequired, -}; -``` - -* Do not use underscores when naming private methods. -* Add descriptions to all `propTypes` using a block comment above the definition. No need to document the types (that's what `propTypes` is doing already), but add some context for each property so that other developers understand the intended use. - -```javascript -// Bad -const propTypes = { - currency: PropTypes.string.isRequired, - amount: PropTypes.number.isRequired, - isIgnored: PropTypes.bool.isRequired +type ComponentProps = { + currency: string; + amount: number; + isIgnored: boolean; }; // Bad -const propTypes = { +type ComponentProps = { // The currency that the reward is in - currency: React.PropTypes.string.isRequired, + currency: string; // The amount of reward - amount: React.PropTypes.number.isRequired, + amount: number; // If the reward has been ignored or not - isIgnored: React.PropTypes.bool.isRequired + isIgnored: boolean; } // Good -const propTypes = { +type ComponentProps = { /** The currency that the reward is in */ - currency: React.PropTypes.string.isRequired, + currency: string; /** The amount of the reward */ - amount: React.PropTypes.number.isRequired, + amount: number; /** If the reward has not been ignored yet */ - isIgnored: React.PropTypes.bool.isRequired + isIgnored: boolean; } ``` -All `propTypes` and `defaultProps` *must* be defined at the **top** of the file in variables called `propTypes` and `defaultProps`. -These variables should then be assigned to the component at the bottom of the file. - -```js -MyComponent.propTypes = propTypes; -MyComponent.defaultProps = defaultProps; -export default MyComponent; -``` - -Any nested `propTypes` e.g. that may appear in a `PropTypes.shape({})` should also be documented. - -```javascript -// Bad -const propTypes = { - /** Session data */ - session: PropTypes.shape({ - authToken: PropTypes.string, - login: PropTypes.string, - }), -} - -// Good -const propTypes = { - /** Session data */ - session: PropTypes.shape({ - - /** Token used to authenticate the user */ - authToken: PropTypes.string, - - /** User email or phone number */ - login: PropTypes.string, - }), -} -``` - -## Inline Ternaries +### Inline Ternaries * Use inline ternary statements when rendering optional pieces of templates. Notice the white space and formatting of the ternary. -```javascript +```tsx // Bad { const optionalTitle = props.title ?
{props.title}
: null; @@ -452,7 +910,7 @@ const propTypes = { } ``` -```javascript +```tsx // Good { return ( @@ -466,7 +924,7 @@ const propTypes = { } ``` -```javascript +```tsx // Good { return ( @@ -481,11 +939,11 @@ const propTypes = { } ``` -### Important Note: +#### Important Note: In React Native, one **must not** attempt to falsey-check a string for an inline ternary. Even if it's in curly braces, React Native will try to render it as a `` node and most likely throw an error about trying to render text outside of a `` component. Use `!!` instead. -```javascript +```tsx // Bad! This will cause a breaking an error on native platforms { return ( @@ -511,68 +969,158 @@ In React Native, one **must not** attempt to falsey-check a string for an inline } ``` -## Function component style +### Function component style When writing a function component, you must ALWAYS add a `displayName` property and give it the same value as the name of the component (this is so it appears properly in the React dev tools) -```javascript - function Avatar(props) {...}; +```tsx +function Avatar(props: AvatarProps) {...}; - Avatar.propTypes = propTypes; - Avatar.defaultProps = defaultProps; - Avatar.displayName = 'Avatar'; +Avatar.displayName = 'Avatar'; - export default Avatar; +export default Avatar; ``` -## Forwarding refs +### Forwarding refs When forwarding a ref define named component and pass it directly to the `forwardRef`. By doing this, we remove potential extra layer in React tree in the form of anonymous component. -```javascript - function FancyInput(props, ref) { - ... - return - } +```tsx +import type {ForwarderRef} from 'react'; + +type FancyInputProps = { + ... +}; - export default React.forwardRef(FancyInput) +function FancyInput(props: FancyInputProps, ref: ForwardedRef) { + ... + return +}; + +export default React.forwardRef(FancyInput) ``` -## Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? +If the ref handle is not available (e.g. `useImperativeHandle` is used) you can define a custom handle type above the component. -Class components are DEPRECATED. Use function components and React hooks. +```tsx +import type {ForwarderRef} from 'react'; +import {useImperativeHandle} from 'react'; -[https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) +type FancyInputProps = { + ... + onButtonPressed: () => void; +}; -## Composition vs Inheritance +type FancyInputHandle = { + onButtonPressed: () => void; +} -From React's documentation - ->Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. ->If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. +function FancyInput(props: FancyInputProps, ref: ForwardedRef) { + useImperativeHandle(ref, () => ({onButtonPressed})); -Use an HOC a.k.a. *[Higher order component](https://reactjs.org/docs/higher-order-components.html)* if you find a use case where you need inheritance. + ... + return ; +}; -If several HOC need to be combined, there is a `compose()` utility. But we should not use this utility when there is only one HOC. +export default React.forwardRef(FancyInput) +``` -```javascript -// Bad -export default compose( - withLocalize, -)(MyComponent); +### Hooks and HOCs -// Good -export default compose( - withLocalize, - withWindowDimensions, -)(MyComponent); +Use hooks whenever possible, avoid using HOCs. -// Good -export default withLocalize(MyComponent) +> Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or `compose` when exporting the component. It also allows us to remove `compose` completely in some components since it has been bringing up some issues with TypeScript. Read the [`compose` usage](#compose-usage) section for further information about the TypeScript issues with `compose`. + +Onyx now provides a `useOnyx` hook that should be used over `withOnyx` HOC. + +```tsx +// BAD +type ComponentOnyxProps = { + session: OnyxEntry; +}; + +type ComponentProps = ComponentOnyxProps & { + someProp: string; +}; + +function Component({session, someProp}: ComponentProps) { + const {windowWidth, windowHeight} = useWindowDimensions(); + const {translate} = useLocalize(); + // component's code +} + +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(Component); + +// GOOD +type ComponentProps = { + someProp: string; +}; + +function Component({someProp}: ComponentProps) { + const [session] = useOnyx(ONYXKEYS.SESSION) + + const {windowWidth, windowHeight} = useWindowDimensions(); + const {translate} = useLocalize(); + // component's code +} ``` +### Stateless components vs Pure Components vs Class based components vs Render Props - When to use what? + +Class components are DEPRECATED. Use function components and React hooks. + +[https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function](https://react.dev/reference/react/Component#migrating-a-component-with-lifecycle-methods-from-a-class-to-a-function) + +### Composition + +Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. + +> Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. + +From React's documentation - +>Props and composition give you all the flexibility you need to customize a component’s look and behavior in an explicit and safe way. Remember that components may accept arbitrary props, including primitive values, React elements, or functions. +>If you want to reuse non-UI functionality between components, we suggest extracting it into a separate JavaScript module. The components may import it and use that function, object, or a class, without extending it. + + ```ts + // BAD + export default compose( + withCurrentUserPersonalDetails, + withReportOrNotFound(), + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + }), + )(Component); + + // GOOD + export default withCurrentUserPersonalDetails( + withReportOrNotFound()( + withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + })(Component), + ), + ); + + // GOOD - alternative to HOC nesting + const ComponentWithOnyx = withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, + })(Component); + const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); + export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); + ``` + **Note:** If you find that none of these approaches work for you, please ask an Expensify engineer for guidance via Slack or GitHub. -## Use Refs Appropriately +### Use Refs Appropriately React's documentation explains refs in [detail](https://reactjs.org/docs/refs-and-the-dom.html). It's important to understand when to use them and how to use them to avoid bugs and hard to maintain code. @@ -580,43 +1128,43 @@ A common mistake with refs is using them to pass data back to a parent component There are several ways to use and declare refs and we prefer the [callback method](https://reactjs.org/docs/refs-and-the-dom.html#callback-refs). -## Are we allowed to use [insert brand new React feature]? Why or why not? +### Are we allowed to use [insert brand new React feature]? Why or why not? 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. -# React Hooks: Frequently Asked Questions +## React Hooks: Frequently Asked Questions -## Are Hooks a Replacement for HOCs or Render Props? +### Are Hooks a Replacement for HOCs or Render Props? In most cases, a custom hook is a better pattern to use than an HOC or Render Prop. They are easier to create, understand, use and document. However, there might still be a case for a HOC e.g. if you have a component that abstracts some conditional rendering logic. -## Should I wrap all my inline functions with `useCallback()` or move them out of the component if they have no dependencies? +### Should I wrap all my inline functions with `useCallback()` or move them out of the component if they have no dependencies? The answer depends on whether you need a stable reference for the function. If there are no dependencies, you could move the function out of the component. If there are dependencies, you could use `useCallback()` to ensure the reference updates only when the dependencies change. However, it's important to note that using `useCallback()` may have a performance penalty, although the trade-off is still debated. You might choose to do nothing at all if there is no obvious performance downside to declaring a function inline. It's recommended to follow the guidance in the [React documentation](https://react.dev/reference/react/useCallback#should-you-add-usecallback-everywhere) and add the optimization only if necessary. If it's not obvious why such an optimization (i.e. `useCallback()` or `useMemo()`) would be used, leave a code comment explaining the reasoning to aid reviewers and future contributors. -## Why does `useState()` sometimes get initialized with a function? +### Why does `useState()` sometimes get initialized with a function? React saves the initial state once and ignores it on the next renders. However, if you pass the result of a function to `useState()` or call a function directly e.g. `useState(doExpensiveThings())` it will *still run on every render*. This can hurt performance depending on what work the function is doing. As an optimization, we can pass an initializer function instead of a value e.g. `useState(doExpensiveThings)` or `useState(() => doExpensiveThings())`. -## Is there an equivalent to `componentDidUpdate()` when using hooks? +### Is there an equivalent to `componentDidUpdate()` when using hooks? The short answer is no. A longer answer is that sometimes we need to check not only that a dependency has changed, but how it has changed in order to run a side effect. For example, a prop had a value of an empty string on a previous render, but now is non-empty. The generally accepted practice is to store the "previous" value in a `ref` so the comparison can be made in a `useEffect()` call. -## Are `useCallback()` and `useMemo()` basically the same thing? +### Are `useCallback()` and `useMemo()` basically the same thing? No! It is easy to confuse `useCallback()` with a memoization helper like `_.memoize()` or `useMemo()` but they are really not the same at all. [`useCallback()` will return a cached function _definition_](https://react.dev/reference/react/useCallback) and will not save us any computational cost of running that function. So, if you are wrapping something in a `useCallback()` and then calling it in the render, then it is better to use `useMemo()` to cache the actual **result** of calling that function and use it directly in the render. -## What is the `exhaustive-deps` lint rule? Can I ignore it? +### What is the `exhaustive-deps` lint rule? Can I ignore it? A `useEffect()` that does not include referenced props or state in its dependency array is [usually a mistake](https://legacy.reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies) as often we want effects to re-run when those dependencies change. However, there are some cases where we might actually only want to re-run the effect when only some of those dependencies change. We determined the best practice here should be to allow disabling the “next line” with a comment `//eslint-disable-next-line react-hooks/exhaustive-deps` and an additional comment explanation so the next developer can understand why the rule was not used. -## Should I declare my components with arrow functions (`const`) or the `function` keyword? +### Should I declare my components with arrow functions (`const`) or the `function` keyword? There are pros and cons of each, but ultimately we have standardized on using the `function` keyword to align things more with modern React conventions. There are also some minor cognitive overhead benefits in that you don't need to think about adding and removing brackets when encountering an implicit return. The `function` syntax also has the benefit of being able to be hoisted where arrow functions do not. -## How do I auto-focus a TextInput using `useFocusEffect()`? +### How do I auto-focus a TextInput using `useFocusEffect()`? -```javascript +```tsx const focusTimeoutRef = useRef(null); useFocusEffect(useCallback(() => { @@ -636,11 +1184,11 @@ This works better than using `onTransitionEnd` because - Note - This is a solution from [this PR](https://github.com/Expensify/App/pull/26415). You can find detailed discussion in comments. -# Onyx Best Practices +## Onyx Best Practices [Onyx Documentation](https://github.com/expensify/react-native-onyx) -## Collection Keys +### Collection Keys Our potentially larger collections of data (reports, policies, etc) are typically stored under collection keys. Collection keys let us group together individual keys vs. storing arrays with multiple objects. In general, **do not add a new collection key if it can be avoided**. There is most likely a more logical place to put the state. And failing to associate a state property with its logical owner is something we consider to be an anti-pattern (unnecessary data structure adds complexity for no value). @@ -648,4 +1196,20 @@ For example, if you are storing a boolean value that could be associated with a **Exception:** There are some [gotchas](https://github.com/expensify/react-native-onyx#merging-data) when working with complex nested array values in Onyx. So, this could be another valid reason to break a property off of its parent object (e.g. `reportActions` are easier to work with as a separate collection). -If you're not sure whether something should have a collection key, reach out in [`#expensify-open-source`](https://expensify.slack.com/archives/C01GTK53T8Q) for additional feedback. +If you're not sure whether something should have a collection key reach out in [`#expensify-open-source`](https://expensify.slack.com/archives/C01GTK53T8Q) for additional feedback. + +## Learning Resources + +### Quickest way to learn TypeScript + +- Get up to speed quickly + - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) + - Go though all examples on the playground. Click on "Example" tab on the top +- Handy Reference + - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) + - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) + - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) +- TypeScript with React + - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) + - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) + - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) diff --git a/contributingGuides/TS_CHEATSHEET.md b/contributingGuides/TS_CHEATSHEET.md index 1e330dafb7cf..77d316bb861d 100644 --- a/contributingGuides/TS_CHEATSHEET.md +++ b/contributingGuides/TS_CHEATSHEET.md @@ -21,8 +21,10 @@ - [1.1](#children-prop) **`props.children`** ```tsx - type WrapperComponentProps = { - children?: React.ReactNode; + import type ChildrenProps from '@src/types/utils/ChildrenProps'; + + type WrapperComponentProps = ChildrenProps & { + ... }; function WrapperComponent({ children }: WrapperComponentProps) { diff --git a/contributingGuides/TS_STYLE.md b/contributingGuides/TS_STYLE.md deleted file mode 100644 index d407019cbed6..000000000000 --- a/contributingGuides/TS_STYLE.md +++ /dev/null @@ -1,769 +0,0 @@ -# Expensify TypeScript Style Guide - -## Table of Contents - -- [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) -- [General Rules](#general-rules) -- [Guidelines](#guidelines) - - [1.1 Naming Conventions](#naming-conventions) - - [1.2 `d.ts` Extension](#d-ts-extension) - - [1.3 Type Alias vs. Interface](#type-alias-vs-interface) - - [1.4 Enum vs. Union Type](#enum-vs-union-type) - - [1.5 `unknown` vs. `any`](#unknown-vs-any) - - [1.6 `T[]` vs. `Array`](#array) - - [1.7 @ts-ignore](#ts-ignore) - - [1.8 Optional chaining and nullish coalescing](#ts-nullish-coalescing) - - [1.9 Type Inference](#type-inference) - - [1.10 JSDoc](#jsdoc) - - [1.11 `propTypes` and `defaultProps`](#proptypes-and-defaultprops) - - [1.12 Utility Types](#utility-types) - - [1.13 `object` Type](#object-type) - - [1.14 Export Prop Types](#export-prop-types) - - [1.15 File Organization](#file-organization) - - [1.16 Reusable Types](#reusable-types) - - [1.17 `.tsx`](#tsx) - - [1.18 No inline prop types](#no-inline-prop-types) - - [1.19 Satisfies operator](#satisfies-operator) - - [1.20 Hooks instead of HOCs](#hooks-instead-of-hocs) - - [1.21 `compose` usage](#compose-usage) - - [1.22 Type imports](#type-imports) - - [1.23 Ref types](#ref-types) -- [Exception to Rules](#exception-to-rules) -- [Communication Items](#communication-items) -- [Migration Guidelines](#migration-guidelines) -- [Learning Resources](#learning-resources) - -## Other Expensify Resources on TypeScript - -- [Expensify TypeScript React Native CheatSheet](./TS_CHEATSHEET.md) -- [Expensify TypeScript PropTypes Conversion Table](./PROPTYPES_CONVERSION_TABLE.md) - -## General Rules - -Strive to type as strictly as possible. - -```ts -type Foo = { - fetchingStatus: "loading" | "success" | "error"; // vs. fetchingStatus: string; - person: { name: string; age: number }; // vs. person: Record; -}; -``` - -## Guidelines - - - -- [1.1](#naming-conventions) **Naming Conventions**: Follow naming conventions specified below - - - Use PascalCase for type names. eslint: [`@typescript-eslint/naming-convention`](https://typescript-eslint.io/rules/naming-convention/) - - ```ts - // BAD - type foo = ...; - type BAR = ...; - - // GOOD - type Foo = ...; - type Bar = ...; - ``` - - - Do not postfix type aliases with `Type`. - - ```ts - // BAD - type PersonType = ...; - - // GOOD - type Person = ...; - ``` - - - Use singular name for union types. - - ```ts - // BAD - type Colors = "red" | "blue" | "green"; - - // GOOD - type Color = "red" | "blue" | "green"; - ``` - - - Use `{ComponentName}Props` pattern for prop types. - - ```ts - // BAD - type Props = { - // component's props - }; - - function MyComponent({}: Props) { - // component's code - } - - // GOOD - type MyComponentProps = { - // component's props - }; - - function MyComponent({}: MyComponentProps) { - // component's code - } - ``` - - - For generic type parameters, use `T` if you have only one type parameter. Don't use the `T`, `U`, `V`... sequence. Make type parameter names descriptive, each prefixed with `T`. - - > Prefix each type parameter name to distinguish them from other types. - - ```ts - // BAD - type KeyValuePair = { key: K; value: U }; - - type Keys = Array; - - // GOOD - type KeyValuePair = { key: TKey; value: TValue }; - - type Keys = Array; - type Keys = Array; - ``` - - - -- [1.2](#d-ts-extension) **`d.ts` Extension**: Do not use `d.ts` file extension even when a file contains only type declarations. Only exceptions are `src/types/global.d.ts` and `src/types/modules/*.d.ts` files in which third party packages and JavaScript's built-in modules (e.g. `window` object) can be modified using module augmentation. Refer to the [Communication Items](#communication-items) section to learn more about module augmentation. - - > Why? Type errors in `d.ts` files are not checked by TypeScript [^1]. - -[^1]: This is because `skipLibCheck` TypeScript configuration is set to `true` in this project. - - - -- [1.3](#type-alias-vs-interface) **Type Alias vs. Interface**: Do not use `interface`. Use `type`. eslint: [`@typescript-eslint/consistent-type-definitions`](https://typescript-eslint.io/rules/consistent-type-definitions/) - - > Why? In TypeScript, `type` and `interface` can be used interchangeably to declare types. Use `type` for consistency. - - ```ts - // BAD - interface Person { - name: string; - } - - // GOOD - type Person = { - name: string; - }; - ``` - - - -- [1.4](#enum-vs-union-type) **Enum vs. Union Type**: Do not use `enum`. Use union types. eslint: [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) - - > Why? Enums come with several [pitfalls](https://blog.logrocket.com/why-typescript-enums-suck/). Most enum use cases can be replaced with union types. - - ```ts - // Most simple form of union type. - type Color = "red" | "green" | "blue"; - function printColors(color: Color) { - console.log(color); - } - - // When the values need to be iterated upon. - import { TupleToUnion } from "type-fest"; - - const COLORS = ["red", "green", "blue"] as const; - type Color = TupleToUnion; // type: 'red' | 'green' | 'blue' - - for (const color of COLORS) { - printColor(color); - } - - // When the values should be accessed through object keys. (i.e. `COLORS.Red` vs. `"red"`) - import { ValueOf } from "type-fest"; - - const COLORS = { - Red: "red", - Green: "green", - Blue: "blue", - } as const; - type Color = ValueOf; // type: 'red' | 'green' | 'blue' - - printColor(COLORS.Red); - ``` - - - -- [1.5](#unknown-vs-any) **`unknown` vs. `any`**: Don't use `any`. Use `unknown` if type is not known beforehand. eslint: [`@typescript-eslint/no-explicit-any`](https://typescript-eslint.io/rules/no-explicit-any/) - - > Why? `any` type bypasses type checking. `unknown` is type safe as `unknown` type needs to be type narrowed before being used. - - ```ts - const value: unknown = JSON.parse(someJson); - if (typeof value === 'string') {...} - else if (isPerson(value)) {...} - ... - ``` - - - -- [1.6](#array) **`T[]` vs. `Array`**: Use `T[]` or `readonly T[]` for simple types (i.e. types which are just primitive names or type references). Use `Array` or `ReadonlyArray` for all other types (union types, intersection types, object types, function types, etc). eslint: [`@typescript-eslint/array-type`](https://typescript-eslint.io/rules/array-type/) - - ```ts - // Array - const a: Array = ["a", "b"]; - const b: Array<{ prop: string }> = [{ prop: "a" }]; - const c: Array<() => void> = [() => {}]; - - // T[] - const d: MyType[] = ["a", "b"]; - const e: string[] = ["a", "b"]; - const f: readonly string[] = ["a", "b"]; - ``` - - - -- [1.7](#ts-ignore) **@ts-ignore**: Do not use `@ts-ignore` or its variant `@ts-nocheck` to suppress warnings and errors. - - > Use `@ts-expect-error` during the migration for type errors that should be handled later. Refer to the [Migration Guidelines](#migration-guidelines) for specific instructions on how to deal with type errors during the migration. eslint: [`@typescript-eslint/ban-ts-comment`](https://typescript-eslint.io/rules/ban-ts-comment/) - - - -- [1.8](#ts-nullish-coalescing) **Optional chaining and nullish coalescing**: Use optional chaining and nullish coalescing instead of the `get` lodash function. eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) - - ```ts - // BAD - import lodashGet from "lodash/get"; - const name = lodashGet(user, "name", "default name"); - - // GOOD - const name = user?.name ?? "default name"; - ``` - - - -- [1.9](#type-inference) **Type Inference**: When possible, allow the compiler to infer type of variables. - - ```ts - // BAD - const foo: string = "foo"; - const [counter, setCounter] = useState(0); - - // GOOD - const foo = "foo"; - const [counter, setCounter] = useState(0); - const [username, setUsername] = useState(undefined); // Username is a union type of string and undefined, and its type cannot be inferred from the default value of undefined - ``` - - For function return types, default to always typing them unless a function is simple enough to reason about its return type. - - > Why? Explicit return type helps catch errors when implementation of the function changes. It also makes it easy to read code even when TypeScript intellisense is not provided. - - ```ts - function simpleFunction(name: string) { - return `hello, ${name}`; - } - - function complicatedFunction(name: string): boolean { - // ... some complex logic here ... - return foo; - } - ``` - - - -- [1.10](#jsdoc) **JSDoc**: Omit comments that are redundant with TypeScript. Do not declare types in `@param` or `@return` blocks. Do not write `@implements`, `@enum`, `@private`, `@override`. eslint: [`jsdoc/no-types`](https://github.com/gajus/eslint-plugin-jsdoc/blob/main/.README/rules/no-types.md) - - > Not all parameters or return values need to be listed in the JSDoc comment. If there is no comment accompanying the parameter or return value, omit it. - - ```ts - // BAD - /** - * @param {number} age - * @returns {boolean} Whether the person is a legal drinking age or nots - */ - function canDrink(age: number): boolean { - return age >= 21; - } - - // GOOD - /** - * @returns Whether the person is a legal drinking age or nots - */ - function canDrink(age: number): boolean { - return age >= 21; - } - ``` - - In the above example, because the parameter `age` doesn't have any accompanying comment, it is completely omitted from the JSDoc. - - - -- [1.11](#proptypes-and-defaultprops) **`propTypes` and `defaultProps`**: Do not use them. Use object destructing to assign default values if necessary. - - > Refer to [the propTypes Migration Table](./PROPTYPES_CONVERSION_TABLE.md) on how to type props based on existing `propTypes`. - - > Assign a default value to each optional prop unless the default values is `undefined`. - - ```tsx - type MyComponentProps = { - requiredProp: string; - optionalPropWithDefaultValue?: number; - optionalProp?: boolean; - }; - - function MyComponent({ - requiredProp, - optionalPropWithDefaultValue = 42, - optionalProp, - }: MyComponentProps) { - // component's code - } - ``` - - - -- [1.12](#utility-types) **Utility Types**: Use types from [TypeScript utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) and [`type-fest`](https://github.com/sindresorhus/type-fest) when possible. - - ```ts - type Foo = { - bar: string; - }; - - // BAD - type ReadOnlyFoo = { - readonly [Property in keyof Foo]: Foo[Property]; - }; - - // GOOD - type ReadOnlyFoo = Readonly; - ``` - - - -- [1.13](#object-type) **`object`**: Don't use `object` type. eslint: [`@typescript-eslint/ban-types`](https://typescript-eslint.io/rules/ban-types/) - - > Why? `object` refers to "any non-primitive type," not "any object". Typing "any non-primitive value" is not commonly needed. - - ```ts - // BAD - const foo: object = [1, 2, 3]; // TypeScript does not error - ``` - - If you know that the type of data is an object but don't know what properties or values it has beforehand, use `Record`. - - > Even though `string` is specified as a key, `Record` type can still accepts objects whose keys are numbers. This is because numbers are converted to strings when used as an object index. Note that you cannot use [symbols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) for `Record`. - - ```ts - function logObject(object: Record) { - for (const [key, value] of Object.entries(object)) { - console.log(`${key}: ${value}`); - } - } - ``` - - - -- [1.14](#export-prop-types) **Prop Types**: Don't use `ComponentProps` to grab a component's prop types. Go to the source file for the component and export prop types from there. Import and use the exported prop types. - - > Don't export prop types from component files by default. Only export it when there is a code that needs to access the prop type directly. - - ```tsx - // MyComponent.tsx - export type MyComponentProps = { - foo: string; - }; - - export default function MyComponent({ foo }: MyComponentProps) { - return {foo}; - } - - // BAD - import { ComponentProps } from "React"; - import MyComponent from "./MyComponent"; - type MyComponentProps = ComponentProps; - - // GOOD - import MyComponent, { MyComponentProps } from "./MyComponent"; - ``` - - - -- [1.15](#file-organization) **File organization**: In modules with platform-specific implementations, create `types.ts` to define shared types. Import and use shared types in each platform specific files. Do not use [`satisfies` operator](#satisfies-operator) for platform-specific implementations, always define shared types that complies with all variants. - - > Why? To encourage consistent API across platform-specific implementations. If you're migrating module that doesn't have a default implement (i.e. `index.ts`, e.g. `getPlatform`), refer to [Migration Guidelines](#migration-guidelines) for further information. - - Utility module example - - ```ts - // types.ts - type GreetingModule = { - getHello: () => string; - getGoodbye: () => string; - }; - - // index.native.ts - import { GreetingModule } from "./types"; - function getHello() { - return "hello from mobile code"; - } - function getGoodbye() { - return "goodbye from mobile code"; - } - const Greeting: GreetingModule = { - getHello, - getGoodbye, - }; - export default Greeting; - - // index.ts - import { GreetingModule } from "./types"; - function getHello() { - return "hello from other platform code"; - } - function getGoodbye() { - return "goodbye from other platform code"; - } - const Greeting: GreetingModule = { - getHello, - getGoodbye, - }; - export default Greeting; - ``` - - Component module example - - ```ts - // types.ts - export type MyComponentProps = { - foo: string; - } - - // index.ios.ts - import { MyComponentProps } from "./types"; - - export MyComponentProps; - export default function MyComponent({ foo }: MyComponentProps) { /* ios specific implementation */ } - - // index.ts - import { MyComponentProps } from "./types"; - - export MyComponentProps; - export default function MyComponent({ foo }: MyComponentProps) { /* Default implementation */ } - ``` - - - -- [1.16](#reusable-types) **Reusable Types**: Reusable type definitions, such as models (e.g., Report), must have their own file and be placed under `src/types/`. The type should be exported as a default export. - - ```ts - // src/types/Report.ts - - type Report = {...}; - - export default Report; - ``` - - - -- [1.17](#tsx) **tsx**: Use `.tsx` extension for files that contain React syntax. - - > Why? It is a widely adopted convention to mark any files that contain React specific syntax with `.jsx` or `.tsx`. - - - -- [1.18](#no-inline-prop-types) **No inline prop types**: Do not define prop types inline for components that are exported. - - > Why? Prop types might [need to be exported from component files](#export-prop-types). If the component is only used inside a file or module and not exported, then inline prop types can be used. - - ```ts - // BAD - export default function MyComponent({ foo, bar }: { foo: string, bar: number }){ - // component implementation - }; - - // GOOD - type MyComponentProps = { foo: string, bar: number }; - export default MyComponent({ foo, bar }: MyComponentProps){ - // component implementation - } - ``` - - - -- [1.19](#satisfies-operator) **Satisfies Operator**: Use the `satisfies` operator when you need to validate that the structure of an expression matches a specific type, without affecting the resulting type of the expression. - - > Why? TypeScript developers often want to ensure that an expression aligns with a certain type, but they also want to retain the most specific type possible for inference purposes. The `satisfies` operator assists in doing both. - - ```ts - // BAD - const sizingStyles = { - w50: { - width: '50%', - }, - mw100: { - maxWidth: '100%', - }, - } as const; - - // GOOD - const sizingStyles = { - w50: { - width: '50%', - }, - mw100: { - maxWidth: '100%', - }, - } satisfies Record; - ``` - - - -- [1.20](#hooks-instead-of-hocs) **Hooks instead of HOCs**: Replace HOCs usage with Hooks whenever possible. - - > Why? Hooks are easier to use (can be used inside the function component), and don't need nesting or `compose` when exporting the component. It also allows us to remove `compose` completely in some components since it has been bringing up some issues with TypeScript. Read the [`compose` usage](#compose-usage) section for further information about the TypeScript issues with `compose`. - - > Note: Because Onyx doesn't provide a hook yet, in a component that accesses Onyx data with `withOnyx` HOC, please make sure that you don't use other HOCs (if applicable) to avoid HOC nesting. - - ```tsx - // BAD - type ComponentOnyxProps = { - session: OnyxEntry; - }; - - type ComponentProps = WindowDimensionsProps & - WithLocalizeProps & - ComponentOnyxProps & { - someProp: string; - }; - - function Component({windowWidth, windowHeight, translate, session, someProp}: ComponentProps) { - // component's code - } - - export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - type ComponentOnyxProps = { - session: OnyxEntry; - }; - - type ComponentProps = ComponentOnyxProps & { - someProp: string; - }; - - function Component({session, someProp}: ComponentProps) { - const {windowWidth, windowHeight} = useWindowDimensions(); - const {translate} = useLocalize(); - // component's code - } - - // There is no hook alternative for withOnyx yet. - export default withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - ``` - - - -- [1.21](#compose-usage) **`compose` usage**: Avoid the usage of `compose` function to compose HOCs in TypeScript files. Use nesting instead. - - > Why? `compose` function doesn't work well with TypeScript when dealing with several HOCs being used in a component, many times resulting in wrong types and errors. Instead, nesting can be used to allow a seamless use of multiple HOCs and result in a correct return type of the compoment. Also, you can use [hooks instead of HOCs](#hooks-instead-of-hocs) whenever possible to minimize or even remove the need of HOCs in the component. - - ```ts - // BAD - export default compose( - withCurrentUserPersonalDetails, - withReportOrNotFound(), - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), - )(Component); - - // GOOD - export default withCurrentUserPersonalDetails( - withReportOrNotFound()( - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component), - ), - ); - - // GOOD - alternative to HOC nesting - const ComponentWithOnyx = withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - })(Component); - const ComponentWithReportOrNotFound = withReportOrNotFound()(ComponentWithOnyx); - export default withCurrentUserPersonalDetails(ComponentWithReportOrNotFound); - ``` - - - -- [1.22](#type-imports) **Type imports/exports**: Always use the `type` keyword when importing/exporting types - - > Why? In order to improve code clarity and consistency and reduce bundle size after typescript transpilation, we enforce the all type imports/exports to contain the `type` keyword. This way, TypeScript can automatically remove those imports from the transpiled JavaScript bundle - - Imports: - ```ts - // BAD - import {SomeType} from './a' - import someVariable from './a' - - import {someVariable, SomeOtherType} from './b' - - // GOOD - import type {SomeType} from './a' - import someVariable from './a' - ``` - - Exports: - ```ts - // BAD - export {SomeType} - export someVariable - // or - export {someVariable, SomeOtherType} - - // GOOD - export type {SomeType} - export someVariable - ``` - -- [1.23](#ref-types) **Ref types**: Avoid using HTML elements while declaring refs. Please use React Native components where possible. React Native Web handles the references on its own. It also extends React Native components with [Interaction API](https://necolas.github.io/react-native-web/docs/interactions/) which should be used to handle Pointer and Mouse events. Exception of this rule is when we explicitly need to use functions available only in DOM and not in React Native, e.g. `getBoundingClientRect`. Then please declare ref type as `union` of React Native component and HTML element. When passing it to React Native component assert it as soon as possible using utility methods declared in `src/types/utils`. - -Normal usage: -```tsx -const ref = useRef(); - - {#DO SOMETHING}}> -``` - -Exceptional usage where DOM methods are necessary: -```tsx -import viewRef from '@src/types/utils/viewRef'; - -const ref = useRef(); - -if (ref.current && 'getBoundingClientRect' in ref.current) { - ref.current.getBoundingClientRect(); -} - - {#DO SOMETHING}}> -``` - -## Exception to Rules - -Most of the rules are enforced in ESLint or checked by TypeScript. If you think your particular situation warrants an exception, post the context in the `#expensify-open-source` Slack channel with your message prefixed with `TS EXCEPTION:`. The internal engineer assigned to the PR should be the one that approves each exception, however all discussion regarding granting exceptions should happen in the public channel instead of the GitHub PR page so that the TS migration team can access them easily. - -When an exception is granted, link the relevant Slack conversation in your PR. Suppress ESLint or TypeScript warnings/errors with comments if necessary. - -This rule will apply until the migration is done. After the migration, discussion on granting exception can happen inside the PR page and doesn't need take place in the Slack channel. - -## Communication Items - -> Comment in the `#expensify-open-source` Slack channel if any of the following situations are encountered. Each comment should be prefixed with `TS ATTENTION:`. Internal engineers will access each situation and prescribe solutions to each case. Internal engineers should refer to general solutions to each situation that follows each list item. - -- I think types definitions in a third party library or JavaScript's built-in module are incomplete or incorrect - -When the library indeed contains incorrect or missing type definitions and it cannot be updated, use module augmentation to correct them. All module augmentation code should be contained in `/src/types/modules/*.d.ts`, each library as a separate file. - -```ts -// external-library-name.d.ts - -declare module "external-library-name" { - interface LibraryComponentProps { - // Add or modify typings - additionalProp: string; - } -} -``` - -## Migration Guidelines - -> This section contains instructions that are applicable during the migration. - -- 🚨 Any new files under `src/` directory MUST be created in TypeScript now! New files in other directories (e.g. `tests/`, `desktop/`) can be created in TypeScript, if desired. - -- If you're migrating a module that doesn't have a default implementation (i.e. `index.ts`, e.g. `getPlatform`), convert `index.website.js` to `index.ts`. Without `index.ts`, TypeScript cannot get type information where the module is imported. - -- Deprecate the usage of `underscore`. Use vanilla methods from JS instead. Only use `lodash` when there is no easy vanilla alternative (eg. `lodashMerge`). eslint: [`no-restricted-imports`](https://eslint.org/docs/latest/rules/no-restricted-imports) - -```ts -// BAD -var arr = []; -_.each(arr, () => {}); - -// GOOD -var arr = []; -arr.forEach(function loopArr() {}); - -// BAD -lodashGet(object, ['foo'], 'bar'); - -// GOOD -object?.foo ?? 'bar'; -``` - -- Found type bugs. Now what? - - If TypeScript migration uncovers a bug that has been “invisible,” there are two options an author of a migration PR can take: - - - Fix issues if they are minor. Document each fix in the PR comment. - - Suppress a TypeScript error stemming from the bug with `@ts-expect-error`. Create a separate GH issue. Prefix the issue title with `[TS ERROR #]`. Cross-link the migration PR and the created GH issue. On the same line as `@ts-expect-error`, put down the GH issue number prefixed with `TODO:`. - - > The `@ts-expect-error` annotation tells the TS compiler to ignore any errors in the line that follows it. However, if there's no error in the line, TypeScript will also raise an error. - - ```ts - // @ts-expect-error TODO: #21647 - const x: number = "123"; // No TS error raised - - // @ts-expect-error - const y: number = 123; // TS error: Unused '@ts-expect-error' directive. - ``` - -- The TS issue I'm working on is blocked by another TS issue because of type errors. What should I do? - - In order to proceed with the migration faster, we are now allowing the use of `@ts-expect-error` annotation to temporally suppress those errors and help you unblock your issues. The only requirements is that you MUST add the annotation with a comment explaining that it must be removed when the blocking issue is migrated, e.g.: - - ```tsx - return ( - - ); - ``` - - **You will also need to reference the blocking issue in your PR.** You can find all the TS issues [here](https://github.com/orgs/Expensify/projects/46). - -## Learning Resources - -### Quickest way to learn TypeScript - -- Get up to speed quickly - - [TypeScript playground](https://www.typescriptlang.org/play?q=231#example) - - Go though all examples on the playground. Click on "Example" tab on the top -- Handy Reference - - [TypeScript CheatSheet](https://www.typescriptlang.org/cheatsheets) - - [Type](https://www.typescriptlang.org/static/TypeScript%20Types-ae199d69aeecf7d4a2704a528d0fd3f9.png) - - [Control Flow Analysis](https://www.typescriptlang.org/static/TypeScript%20Control%20Flow%20Analysis-8a549253ad8470850b77c4c5c351d457.png) -- TypeScript with React - - [React TypeScript CheatSheet](https://react-typescript-cheatsheet.netlify.app/) - - [List of built-in utility types](https://react-typescript-cheatsheet.netlify.app/docs/basic/troubleshooting/utilities) - - [HOC CheatSheet](https://react-typescript-cheatsheet.netlify.app/docs/hoc/) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 749785c8698b..4120e6accb86 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.71 + 1.4.72 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.71.4 + 1.4.72.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index c71854dfcae1..11edf545cea4 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.71 + 1.4.72 CFBundleSignature ???? CFBundleVersion - 1.4.71.4 + 1.4.72.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index b06211595ad0..1f777d1739e4 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.71 + 1.4.72 CFBundleVersion - 1.4.71.4 + 1.4.72.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index a57d1ad8760e..e2d9b6d2988b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.71-4", + "version": "1.4.72-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.71-4", + "version": "1.4.72-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dd7207e808ef..c91a7a6acc4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.71-4", + "version": "1.4.72-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.", diff --git a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx index 58a116789bbe..b82fd55dc038 100644 --- a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx +++ b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext.tsx @@ -5,7 +5,7 @@ type CustomStatusBarAndBackgroundContextType = { setRootStatusBarEnabled: (isEnabled: boolean) => void; }; -// Signin page has its seperate Statusbar and ThemeProvider, so when user is on the SignInPage we need to disable the root statusbar so there is no double status bar in component stack, first in Root and other in SignInPage +// Signin page has its separate Statusbar and ThemeProvider, so when user is on the SignInPage we need to disable the root statusbar so there is no double status bar in component stack, first in Root and other in SignInPage const CustomStatusBarAndBackgroundContext = createContext({isRootStatusBarEnabled: true, setRootStatusBarEnabled: () => undefined}); export default CustomStatusBarAndBackgroundContext; diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 2c592c20f4c6..10fb0430dec7 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -102,9 +102,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & /** Callback to inform a participant is selected */ onSelectParticipant?: (option: Participant) => void; - /** Should we request a single or multiple participant selection from user */ - hasMultipleParticipants: boolean; - /** IOU amount */ iouAmount: number; @@ -209,7 +206,6 @@ function MoneyRequestConfirmationList({ policyTags, iouCurrencyCode, iouMerchant, - hasMultipleParticipants, selectedParticipants: selectedParticipantsProp, payeePersonalDetails: payeePersonalDetailsProp, session, @@ -466,6 +462,9 @@ function MoneyRequestConfirmationList({ const shouldShowReadOnlySplits = useMemo(() => isPolicyExpenseChat || isReadOnly || isScanRequest, [isPolicyExpenseChat, isReadOnly, isScanRequest]); const splitParticipants = useMemo(() => { + if (!isTypeSplit) { + return; + } const payeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails); if (shouldShowReadOnlySplits) { return [payeeOption, ...selectedParticipants].map((participantOption: Participant) => { @@ -501,7 +500,7 @@ function MoneyRequestConfirmationList({ onAmountChange: (value: string) => onSplitShareChange(participantOption.accountID ?? 0, Number(value)), }, })); - }, [transaction, iouCurrencyCode, onSplitShareChange, payeePersonalDetails, selectedParticipants, currencyList, iouAmount, shouldShowReadOnlySplits, StyleUtils]); + }, [isTypeSplit, transaction, iouCurrencyCode, onSplitShareChange, payeePersonalDetails, selectedParticipants, currencyList, iouAmount, shouldShowReadOnlySplits, StyleUtils]); const isSplitModified = useMemo(() => { if (!transaction?.splitShares) { @@ -512,7 +511,7 @@ function MoneyRequestConfirmationList({ const optionSelectorSections = useMemo(() => { const sections = []; - if (hasMultipleParticipants) { + if (isTypeSplit) { sections.push( ...[ { @@ -543,14 +542,14 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [selectedParticipants, hasMultipleParticipants, translate, splitParticipants, transaction, shouldShowReadOnlySplits, isSplitModified, payeePersonalDetails]); + }, [selectedParticipants, isTypeSplit, translate, splitParticipants, transaction, shouldShowReadOnlySplits, isSplitModified, payeePersonalDetails]); const selectedOptions = useMemo(() => { - if (!hasMultipleParticipants) { + if (!isTypeSplit) { return []; } return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); + }, [selectedParticipants, isTypeSplit, payeePersonalDetails]); useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { diff --git a/src/languages/es.ts b/src/languages/es.ts index c9e14df87f70..48338fdf8c06 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -184,7 +184,7 @@ export default { dob: 'Fecha de nacimiento', currentYear: 'Año actual', currentMonth: 'Mes actual', - ssnLast4: 'Últimos 4 dígitos de su SSN', + ssnLast4: 'Últimos 4 dígitos de tu SSN', ssnFull9: 'Los 9 dígitos del SSN', addressLine: ({lineNumber}: AddressLineParams) => `Dirección línea ${lineNumber}`, personalAddress: 'Dirección física personal', @@ -538,9 +538,9 @@ export default { }, reportArchiveReasons: { [CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'Esta sala de chat ha sido eliminada.', - [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}: ReportArchiveReasonsClosedParams) => `Este chat está desactivado porque ${displayName} ha cerrado su cuenta.`, + [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}: ReportArchiveReasonsClosedParams) => `Este chat está desactivado porque ${displayName} ha cerrado tu cuenta.`, [CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}: ReportArchiveReasonsMergedParams) => - `Este chat está desactivado porque ${oldDisplayName} ha combinado su cuenta con ${displayName}`, + `Este chat está desactivado porque ${oldDisplayName} ha combinado tu cuenta con ${displayName}`, [CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName, shouldUseYou = false}: ReportArchiveReasonsRemovedFromPolicyParams) => shouldUseYou ? `Este chat ya no está activo porque tu ya no eres miembro del espacio de trabajo ${policyName}.` @@ -682,7 +682,7 @@ export default { waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, adminCanceledRequest: ({manager, amount}: AdminCanceledRequestParams) => `${manager ? `${manager}: ` : ''}canceló el pago de ${amount}.`, canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => - `canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`, + `canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó tu billetera Expensify en un plazo de 30 días.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount} de otra forma`, @@ -725,7 +725,7 @@ export default { splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, - waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, + waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active tu Billetera`, enableWallet: 'Habilitar Billetera', holdExpense: 'Bloquear gasto', unholdExpense: 'Desbloquear gasto', @@ -774,7 +774,7 @@ export default { editImage: 'Editar foto', viewPhoto: 'Ver foto', imageUploadFailed: 'Error al cargar la imagen', - deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de su espacio de trabajo.', + deleteWorkspaceError: 'Lo sentimos, hubo un problema eliminando el avatar de tu espacio de trabajo.', sizeExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `La imagen supera el tamaño máximo de ${maxUploadSizeInMB}MB.`, resolutionConstraints: ({minHeightInPx, minWidthInPx, maxHeightInPx, maxWidthInPx}: ResolutionConstraintsParams) => `Por favor, elige una imagen más grande que ${minHeightInPx}x${minWidthInPx} píxeles y más pequeña que ${maxHeightInPx}x${maxWidthInPx} píxeles.`, @@ -867,7 +867,7 @@ export default { }, displayNamePage: { headerTitle: 'Nombre', - isShownOnProfile: 'Este nombre es visible en su perfil.', + isShownOnProfile: 'Este nombre es visible en tu perfil.', }, timezonePage: { timezone: 'Zona horaria', @@ -984,7 +984,7 @@ export default { stepVerify: 'Verificar', scanCode: 'Escanea el código QR usando tu', authenticatorApp: 'aplicación de autenticación', - addKey: 'O añade esta clave secreta a su aplicación de autenticación:', + addKey: 'O añade esta clave secreta a tu aplicación de autenticación:', enterCode: 'Luego introduce el código de seis dígitos generado por tu aplicación de autenticación.', stepSuccess: 'Finalizado', enabled: '¡La autenticación de dos factores está ahora habilitada!', @@ -1072,7 +1072,7 @@ export default { assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del Espacio de Trabajo para gestionar los gastos de la empresa.', expensifyCard: 'Tarjeta Expensify', - walletActivationPending: 'Estamos revisando su información, por favor vuelve en unos minutos.', + walletActivationPending: 'Estamos revisando tu información, por favor vuelve en unos minutos.', walletActivationFailed: 'Lamentablemente, no podemos activar tu billetera en este momento. Chatea con Concierge para obtener más ayuda.', addYourBankAccount: 'Añadir tu cuenta bancaria.', addBankAccountBody: 'Conectemos tu cuenta bancaria a Expensify para que sea más fácil que nunca enviar y recibir pagos directamente en la aplicación.', @@ -1405,8 +1405,8 @@ export default { `Nuestro proveedor de correo electrónico ha suspendido temporalmente los correos electrónicos a ${login} debido a problemas de entrega. Para desbloquear el inicio de sesión, sigue estos pasos:`, confirmThat: ({login}: ConfirmThatParams) => `Confirma que ${login} está escrito correctamente y que es una dirección de correo electrónico real que puede recibir correos. `, emailAliases: - 'Los alias de correo electrónico como "expenses@domain.com" deben tener acceso a su propia bandeja de entrada de correo electrónico para que sea un inicio de sesión válido de Expensify.', - ensureYourEmailClient: 'Asegúrese de que su cliente de correo electrónico permita correos electrónicos de expensify.com. ', + 'Los alias de correo electrónico como "expenses@domain.com" deben tener acceso a tu propia bandeja de entrada de correo electrónico para que sea un inicio de sesión válido de Expensify.', + ensureYourEmailClient: 'Asegúrese de que tu cliente de correo electrónico permita correos electrónicos de expensify.com. ', youCanFindDirections: 'Puedes encontrar instrucciones sobre cómo completar este paso ', helpConfigure: ', pero es posible que necesites que el departamento de informática te ayude a configurar los ajustes de correo electrónico.', onceTheAbove: 'Una vez completados los pasos anteriores, ponte en contacto con ', @@ -1513,7 +1513,7 @@ export default { yourDataIsSecure: 'Tus datos están seguros', toGetStarted: 'Añade una cuenta bancaria y emite tarjetas corporativas, reembolsa gastos y cobra y paga facturas, todo desde un mismo lugar.', plaidBodyCopy: 'Ofrezca a sus empleados una forma más sencilla de pagar - y recuperar - los gastos de la empresa.', - checkHelpLine: 'Su número de ruta y número de cuenta se pueden encontrar en un cheque de la cuenta bancaria.', + checkHelpLine: 'Tu número de ruta y número de cuenta se pueden encontrar en un cheque de la cuenta bancaria.', validateAccountError: { phrase1: '¡Un momento! Primero necesitas validar tu cuenta. Para hacerlo, ', phrase2: 'vuelve a iniciar sesión con un código mágico', @@ -1582,7 +1582,7 @@ export default { userIsAlreadyMember: ({login, name}: UserIsAlreadyMemberParams) => `${login} ya es miembro de ${name}`, }, onfidoStep: { - acceptTerms: 'Al continuar con la solicitud para activar su billetera Expensify, confirma que ha leído, comprende y acepta ', + acceptTerms: 'Al continuar con la solicitud para activar tu billetera Expensify, confirma que ha leído, comprende y acepta ', facialScan: 'Política y lanzamiento de la exploración facial de Onfido', tryAgain: 'Intentar otra vez', verifyIdentity: 'Verificar identidad', @@ -1877,7 +1877,7 @@ export default { termsAndConditions: { header: 'Antes de continuar...', title: 'Por favor, lee los Términos y condiciones para reservar viajes', - subtitle: 'Para permitir la opción de reservar viajes en su espacio de trabajo debe aceptar nuestros ', + subtitle: 'Para permitir la opción de reservar viajes en tu espacio de trabajo debe aceptar nuestros ', termsconditions: 'términos y condiciones', travelTermsAndConditions: 'términos y condiciones de viaje', helpDocIntro: 'Consulta este ', @@ -1972,7 +1972,7 @@ export default { }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { label: 'Fecha de envío', - description: 'Fecha en la que el informe se envió para su aprobación', + description: 'Fecha en la que el informe se envió para tu aprobación', }, }, }, @@ -1991,10 +1991,10 @@ export default { 'Nota: QuickBooks Online no admite un campo para Ubicaciones como etiquetas en las exportaciones de facturas de proveedores. A medida que importa ubicaciones, esta opción de exportación no está disponible.', exportPreferredExporterNote: 'Puede ser cualquier administrador del espacio de trabajo, pero debe ser un administrador de dominio si configura diferentes cuentas de exportación para tarjetas de empresa individuales en la configuración del dominio.', - exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en su cuenta.', + exportPreferredExporterSubNote: 'Una vez configurado, el exportador preferido verá los informes para exportar en tu cuenta.', exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Online.', exportVendorBillDescription: - 'Crearemos una única factura de proveedor detallada para cada informe de Expensify. Si el período de la factura está cerrado, lo publicaremos en el día 1 del siguiente período abierto. Puede agregar la factura del proveedor a la cuenta A/P de su elección (a continuación).', + 'Crearemos una única factura de proveedor detallada para cada informe de Expensify. Si el período de la factura está cerrado, lo publicaremos en el día 1 del siguiente período abierto. Puede agregar la factura del proveedor a la cuenta A/P de tu elección (a continuación).', outOfPocketTaxEnabledDescription: 'Nota: QuickBooks Online no admite un campo para impuestos en las exportaciones de Anotación en el diario. Debido a que tienes habilitado el seguimiento de impuestos en tu área de trabajo, esta opción de exportación no está disponible.', outOfPocketTaxEnabledError: 'La Anotacion en el diario no está disponible cuando los impuestos están activados. Por favor, selecciona una opción de exportación diferente.', @@ -2033,7 +2033,7 @@ export default { [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]: "Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de crédito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Credit Card Misc.'.", [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]: - 'Crearemos una única factura detallada con los proveedores por cada informe de Expensify, con fecha del último gasto en el informe. Si este período está cerrado, la publicaremos con fecha del día 1 del próximo período abierto. Puede añadir la factura del proveedor a la cuenta A/P de su elección (a continuación).', + 'Crearemos una única factura detallada con los proveedores por cada informe de Expensify, con fecha del último gasto en el informe. Si este período está cerrado, la publicaremos con fecha del día 1 del próximo período abierto. Puede añadir la factura del proveedor a la cuenta A/P de tu elección (a continuación).', [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD}AccountDescription`]: 'Las transacciones con tarjeta de débito se exportarán a la cuenta bancaria que aparece a continuación.”', @@ -2563,10 +2563,10 @@ export default { }, changeOwner: { changeOwnerPageTitle: 'Dueño de la transferencia', - addPaymentCardTitle: 'Ingrese su tarjeta de pago para transferir la propiedad', + addPaymentCardTitle: 'Ingrese tu tarjeta de pago para transferir la propiedad', addPaymentCardButtonText: 'Aceptar términos y agregar tarjeta de pago', addPaymentCardReadAndAcceptTextPart1: 'Lea y acepte', - addPaymentCardReadAndAcceptTextPart2: 'para agregar su tarjeta', + addPaymentCardReadAndAcceptTextPart2: 'para agregar tu tarjeta', addPaymentCardTerms: 'los términos', addPaymentCardPrivacy: 'la política de privacidad', addPaymentCardAnd: 'y', @@ -2582,15 +2582,15 @@ export default { ownerOwesAmountTitle: 'Saldo pendiente', ownerOwesAmountButtonText: 'Transferir saldo', ownerOwesAmountText: ({email, amount}) => - `La cuenta propietaria de este espacio de trabajo (${email}) tiene un saldo pendiente de un mes anterior.\n\n¿Desea transferir este monto (${amount}) para hacerse cargo de la facturación de este espacio de trabajo? Su tarjeta de pago se cargará inmediatamente.`, + `La cuenta propietaria de este espacio de trabajo (${email}) tiene un saldo pendiente de un mes anterior.\n\n¿Desea transferir este monto (${amount}) para hacerse cargo de la facturación de este espacio de trabajo? tu tarjeta de pago se cargará inmediatamente.`, subscriptionTitle: 'Asumir la suscripción anual', subscriptionButtonText: 'Transferir suscripción', subscriptionText: ({usersCount, finalCount}) => - `Al hacerse cargo de este espacio de trabajo se fusionará su suscripción anual asociada con su suscripción actual. Esto aumentará el tamaño de su suscripción en ${usersCount} usuarios, lo que hará que su nuevo tamaño de suscripción sea ${finalCount}. ¿Te gustaria continuar?`, + `Al hacerse cargo de este espacio de trabajo se fusionará tu suscripción anual asociada con tu suscripción actual. Esto aumentará el tamaño de tu suscripción en ${usersCount} usuarios, lo que hará que tu nuevo tamaño de suscripción sea ${finalCount}. ¿Te gustaria continuar?`, duplicateSubscriptionTitle: 'Alerta de suscripción duplicada', duplicateSubscriptionButtonText: 'Continuar', duplicateSubscriptionText: ({email, workspaceName}) => - `Parece que estás intentando hacerte cargo de la facturación de los espacios de trabajo de ${email}, pero para hacerlo, primero debes ser administrador de todos sus espacios de trabajo.\n\nHaz clic en "Continuar" si solo quieres tomar sobrefacturación para el espacio de trabajo ${workspaceName}.\n\nSi desea hacerse cargo de la facturación de toda su suscripción, pídales que lo agreguen como administrador a todos sus espacios de trabajo antes de hacerse cargo de la facturación.`, + `Parece que estás intentando hacerte cargo de la facturación de los espacios de trabajo de ${email}, pero para hacerlo, primero debes ser administrador de todos sus espacios de trabajo.\n\nHaz clic en "Continuar" si solo quieres tomar sobrefacturación para el espacio de trabajo ${workspaceName}.\n\nSi desea hacerse cargo de la facturación de toda tu suscripción, pídales que lo agreguen como administrador a todos sus espacios de trabajo antes de hacerse cargo de la facturación.`, hasFailedSettlementsTitle: 'No se puede transferir la propiedad', hasFailedSettlementsButtonText: 'Entiendo', hasFailedSettlementsText: ({email}) => @@ -3365,7 +3365,7 @@ export default { nothing: 'Por ahora, nada', }, moderation: { - flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.', + flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para tu revisión.', chooseAReason: 'Elige abajo un motivo para reportarlo:', spam: 'Spam', spamDescription: 'Publicidad no solicitada', @@ -3391,7 +3391,7 @@ export default { joinExpensifyOrg: 'Únete a Expensify.org para eliminar la injusticia en todo el mundo y ayuda a los profesores a dividir sus gastos para las aulas más necesitadas.', iKnowATeacher: 'Yo conozco a un profesor', iAmATeacher: 'Soy profesor', - getInTouch: '¡Excelente! Por favor, comparte su información para que podamos ponernos en contacto con ellos.', + getInTouch: '¡Excelente! Por favor, comparte tu información para que podamos ponernos en contacto con ellos.', introSchoolPrincipal: 'Introducción al director del colegio', schoolPrincipalVerfiyExpense: 'Expensify.org divide el coste del material escolar esencial para que los estudiantes de familias con bajos ingresos puedan tener una mejor experiencia de aprendizaje. Se pedirá a tu director que verifique tus gastos.', @@ -3520,15 +3520,15 @@ export default { overLimit: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, - receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud', + receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma tu exactitud', receiptRequired: (params: ViolationsReceiptRequiredParams) => `Recibo obligatorio${params ? ` para importes sobre${params.formattedLimit ? ` ${params.formattedLimit}` : ''}${params.category ? ' el límite de la categoría' : ''}` : ''}`, reviewRequired: 'Revisión requerida', rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin - ? `No se puede adjuntar recibo debido a un problema con la conexión a su banco que ${email} necesita arreglar` - : 'No se puede adjuntar recibo debido a un problema con la conexión a su banco que necesitas arreglar'; + ? `No se puede adjuntar recibo debido a un problema con la conexión a tu banco que ${email} necesita arreglar` + : 'No se puede adjuntar recibo debido a un problema con la conexión a tu banco que necesitas arreglar'; } if (!isTransactionOlderThan7Days) { return isAdmin @@ -3575,7 +3575,7 @@ export default { [CONST.EXIT_SURVEY.REASONS.DONT_UNDERSTAND]: '¿Qué estás tratando de hacer?', [CONST.EXIT_SURVEY.REASONS.PREFER_CLASSIC]: '¿Por qué prefieres Expensify Classic?', }, - responsePlaceholder: 'Su respuesta', + responsePlaceholder: 'Tu respuesta', thankYou: '¡Gracias por tus comentarios!', thankYouSubtitle: 'Sus respuestas nos ayudarán a crear un mejor producto para hacer las cosas bien. ¡Muchas gracias!', goToExpensifyClassic: 'Cambiar a Expensify Classic', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0154bef51f2c..906a2963baa8 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -20,7 +20,7 @@ import type { OriginalMessageReimbursementDequeued, } from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; -import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; +import type {Message, ReportActionBase, ReportActionMessageJSON, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -143,8 +143,27 @@ function isModifiedExpenseAction(reportAction: OnyxEntry | ReportA return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE; } +/** + * We are in the process of deprecating reportAction.originalMessage and will be setting the db version of "message" to reportAction.message in the future see: https://github.com/Expensify/App/issues/39797 + * In the interim, we must check to see if we have an object or array for the reportAction.message, if we have an array we will use the originalMessage as this means we have not yet migrated. + */ +function getWhisperedTo(reportAction: OnyxEntry | EmptyObject): number[] { + const originalMessage = reportAction?.originalMessage; + const message = reportAction?.message; + + if (!Array.isArray(message) && typeof message === 'object') { + return (message as ReportActionMessageJSON)?.whisperedTo ?? []; + } + + if (originalMessage) { + return (originalMessage as ReportActionMessageJSON)?.whisperedTo ?? []; + } + + return []; +} + function isWhisperAction(reportAction: OnyxEntry | EmptyObject): boolean { - return (reportAction?.whisperedToAccountIDs ?? []).length > 0; + return getWhisperedTo(reportAction).length > 0; } /** @@ -154,7 +173,7 @@ function isWhisperActionTargetedToOthers(reportAction: OnyxEntry): if (!isWhisperAction(reportAction)) { return false; } - return !reportAction?.whisperedToAccountIDs?.includes(currentUserAccountID ?? 0); + return !getWhisperedTo(reportAction).includes(currentUserAccountID ?? 0); } function isReimbursementQueuedAction(reportAction: OnyxEntry) { @@ -278,8 +297,8 @@ function shouldIgnoreGap(currentReportAction: ReportAction | undefined, nextRepo return ( isOptimisticAction(currentReportAction) || isOptimisticAction(nextReportAction) || - !!currentReportAction.whisperedToAccountIDs?.length || - !!nextReportAction.whisperedToAccountIDs?.length || + !!getWhisperedTo(currentReportAction).length || + !!getWhisperedTo(nextReportAction).length || currentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM || nextReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || nextReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED @@ -1196,6 +1215,7 @@ export { getParentReportAction, getReportAction, getReportActionMessageText, + getWhisperedTo, isApprovedOrSubmittedReportAction, getReportPreviewAction, getSortedReportActions, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 64b77cae6313..4318f3c4463e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -51,7 +51,7 @@ import type { } from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report'; -import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; +import type {Message, ReportActionBase, ReportActions, ReportPreviewAction} from '@src/types/onyx/ReportAction'; import type {Comment, Receipt, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -148,7 +148,6 @@ type OptimisticAddCommentReportAction = Pick< | 'childCommenterCount' | 'childLastVisibleActionCreated' | 'childOldestFourAccountIDs' - | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -197,7 +196,6 @@ type OptimisticIOUReportAction = Pick< | 'created' | 'pendingAction' | 'receipt' - | 'whisperedToAccountIDs' | 'childReportID' | 'childVisibleActionCount' | 'childCommenterCount' @@ -3074,6 +3072,12 @@ function getReportActionMessage(reportAction: ReportAction | EmptyObject, parent if (isEmptyObject(reportAction)) { return ''; } + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { + return Localize.translateLocal('iou.heldExpense'); + } + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { + return Localize.translateLocal('iou.unheldExpense'); + } if (ReportActionsUtils.isApprovedOrSubmittedReportAction(reportAction)) { return ReportActionsUtils.getReportActionMessageText(reportAction); } @@ -3898,6 +3902,7 @@ function buildOptimisticIOUReportAction( IOUTransactionID: transactionID, IOUReportID, type, + whisperedTo: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID ?? -1] : [], }; if (type === CONST.IOU.REPORT_ACTION_TYPE.PAY) { @@ -3951,7 +3956,6 @@ function buildOptimisticIOUReportAction( shouldShow: true, created, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - whisperedToAccountIDs: [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === receipt?.state) ? [currentUserAccountID ?? -1] : [], }; } @@ -4083,6 +4087,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, originalMessage: { linkedReportID: iouReport?.reportID, + whisperedTo: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }, message: [ { @@ -4100,7 +4105,6 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, - whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID ?? -1] : [], }; } @@ -4190,7 +4194,13 @@ function buildOptimisticMovedTrackedExpenseModifiedReportAction(transactionThrea * @param [transaction] - optimistic newest transaction of a report preview * */ -function updateReportPreview(iouReport: OnyxEntry, reportPreviewAction: ReportAction, isPayRequest = false, comment = '', transaction: OnyxEntry = null): ReportAction { +function updateReportPreview( + iouReport: OnyxEntry, + reportPreviewAction: ReportPreviewAction, + isPayRequest = false, + comment = '', + transaction: OnyxEntry = null, +): ReportPreviewAction { const hasReceipt = TransactionUtils.hasReceipt(transaction); const recentReceiptTransactions = reportPreviewAction?.childRecentReceiptTransactionIDs ?? {}; const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions); @@ -4226,7 +4236,10 @@ function updateReportPreview(iouReport: OnyxEntry, reportPreviewAction: : recentReceiptTransactions, // As soon as we add a transaction without a receipt to the report, it will have ready expenses, // so we remove the whisper - whisperedToAccountIDs: hasReceipt ? reportPreviewAction?.whisperedToAccountIDs : [], + originalMessage: { + ...(reportPreviewAction.originalMessage ?? {}), + whisperedTo: hasReceipt ? reportPreviewAction?.originalMessage?.whisperedTo : [], + }, }; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7171a7d6732a..eb7b82800358 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -55,6 +55,7 @@ import type {Participant, Split} from '@src/types/onyx/IOU'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {IOUMessage, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type ReportAction from '@src/types/onyx/ReportAction'; +import type {ReportPreviewAction} from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {Comment, Receipt, ReceiptSource, SplitShares, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -1875,7 +1876,7 @@ function getMoneyRequestInformation( let reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { - reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, false, comment, optimisticTransaction); + reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction as ReportPreviewAction, false, comment, optimisticTransaction); } else { reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction); @@ -2099,7 +2100,7 @@ function getTrackExpenseInformation( reportPreviewAction = shouldCreateNewMoneyRequestReport ? null : getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { - reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, false, comment, optimisticTransaction); + reportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction as ReportPreviewAction, false, comment, optimisticTransaction); } else { reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, iouReport, comment, optimisticTransaction); // Generated ReportPreview action is a parent report action of the iou report. @@ -2444,7 +2445,9 @@ function getUpdateMoneyRequestParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, value: { [transactionThread?.parentReportActionID ?? '']: { - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }, @@ -2453,7 +2456,9 @@ function getUpdateMoneyRequestParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.parentReportID}`, value: { [iouReport?.parentReportActionID ?? '']: { - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }, @@ -2687,7 +2692,9 @@ function getUpdateTrackExpenseParams( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, value: { [transactionThread?.parentReportActionID ?? '']: { - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }); @@ -3949,7 +3956,7 @@ function createSplitsAndOnyxData( let oneOnOneReportPreviewAction = getReportPreviewAction(oneOnOneChatReport.reportID, oneOnOneIOUReport.reportID); if (oneOnOneReportPreviewAction) { - oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction); + oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction as ReportPreviewAction); } else { oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport); } @@ -4509,7 +4516,9 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA value: { [reportAction.reportActionID]: { lastModified: DateUtils.getDBTime(), - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }, @@ -4636,7 +4645,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA let oneOnOneReportPreviewAction = getReportPreviewAction(oneOnOneChatReport?.reportID ?? '', oneOnOneIOUReport?.reportID ?? ''); if (oneOnOneReportPreviewAction) { - oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction); + oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction as ReportPreviewAction); } else { oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport, '', oneOnOneTransaction); } @@ -4825,7 +4834,9 @@ function editRegularMoneyRequest( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, value: { [transactionThread?.parentReportActionID ?? '']: { - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }, @@ -4834,7 +4845,9 @@ function editRegularMoneyRequest( key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.parentReportID}`, value: { [iouReport?.parentReportActionID ?? '']: { - whisperedToAccountIDs: [], + originalMessage: { + whisperedTo: [], + }, }, }, }, @@ -5673,7 +5686,7 @@ function getPayMoneyRequestParams( let optimisticReportPreviewAction = null; const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID); if (reportPreviewAction) { - optimisticReportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction, true); + optimisticReportPreviewAction = ReportUtils.updateReportPreview(iouReport, reportPreviewAction as ReportPreviewAction, true); } const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${iouReport.reportID}`] ?? null; diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index 6d51d3ac02a6..04e39610ed6b 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -129,6 +129,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa // Prevent info loops of calls to GetMissingOnyxMessages if (previousParams?.newLastUpdateIDFromClient === newLastUpdateIDFromClient && previousParams?.latestMissingUpdateID === latestMissingUpdateID) { Log.info('[DeferredUpdates] Aborting call to GetMissingOnyxMessages, repeated params', false, {lastUpdateIDFromClient, latestMissingUpdateID, previousParams}); + resolve(undefined); return; } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index fddbbadde33c..d8b1bc774de0 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -918,12 +918,12 @@ function ReportActionItem({ } const hasErrors = !isEmptyObject(action.errors); - const whisperedToAccountIDs = action.whisperedToAccountIDs ?? []; - const isWhisper = whisperedToAccountIDs.length > 0; - const isMultipleParticipant = whisperedToAccountIDs.length > 1; - const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); + const whisperedTo = ReportActionsUtils.getWhisperedTo(action); + const isWhisper = whisperedTo.length > 0; + const isMultipleParticipant = whisperedTo.length > 1; + const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedTo); const whisperedToPersonalDetails = isWhisper - ? (Object.values(personalDetails ?? {}).filter((details) => whisperedToAccountIDs.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) + ? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? -1)) as OnyxTypes.PersonalDetails[]) : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; @@ -993,7 +993,7 @@ function ReportActionItem({   ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)) || ReportUtils.isGroupPolicy(policy?.type ?? ''), [report, policy]); + const isPolicyExpenseChat = useMemo(() => participants?.some((participant) => participant.isPolicyExpenseChat), [participants]); const formHasBeenSubmitted = useRef(false); useEffect(() => { @@ -558,7 +558,6 @@ function IOURequestStepConfirmation({ /> ; type ReportAction = ReportActionBase & OriginalMessage; +type ReportPreviewAction = ReportActionBase & OriginalMessageReportPreview; +type ModifiedExpenseAction = ReportActionBase & OriginalMessageModifiedExpense; type ReportActions = Record; type ReportActionsCollectionDataSet = CollectionDataSet; export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionsCollectionDataSet}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionsCollectionDataSet, ReportPreviewAction, ModifiedExpenseAction, ReportActionMessageJSON}; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index d0ea948bdb6c..1cab8f563b24 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -5,6 +5,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; +import type {ModifiedExpenseAction} from '@src/types/onyx/ReportAction'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; @@ -769,8 +770,10 @@ describe('ReportUtils', () => { it("should disable on a whisper action and it's neither a report preview nor IOU action", () => { const reportAction = { actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, - whisperedToAccountIDs: [123456], - } as ReportAction; + originalMessage: { + whisperedTo: [123456], + }, + } as ModifiedExpenseAction; expect(ReportUtils.shouldDisableThread(reportAction, reportID)).toBeTruthy(); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index abfaf9bd8b00..2a9f9074125b 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -158,7 +158,6 @@ function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = text: 'Email One', }, ], - whisperedToAccountIDs: [], automatic: false, message: [ { @@ -182,6 +181,7 @@ function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = }, ], originalMessage: { + whisperedTo: [], childReportID: `${reportActionID}`, emojiReactions: { heart: { diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index 3948baca3113..b9dc50aecec0 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -30,6 +30,7 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi lastModified: '2021-07-14T15:00:00Z', // IOUReportID: index, linkedReportID: index.toString(), + whisperedTo: [], }, pendingAction: null, person: [ @@ -45,7 +46,6 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi sequenceNumber: 0, shouldShow: true, timestamp: 1696243169, - whisperedToAccountIDs: [], } as ReportAction); const getMockedSortedReportActions = (length = 100): ReportAction[] => diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index f19e939083d2..65cbb3ba966e 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -68,8 +68,8 @@ export default function createRandomReportAction(index: number): ReportAction { originalMessage: { html: randWord(), lastModified: getRandomDate(), + whisperedTo: randAggregation(), }, - whisperedToAccountIDs: randAggregation(), avatar: randWord(), automatic: randBoolean(), shouldShow: randBoolean(),