diff --git a/.eslintignore b/.eslintignore index d3358a02fe4b..5338df11c520 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,11 @@ -**/node_modules/* -**/dist/* -android/**/build/* -.github/actions/**/index.js" +!.storybook +!.github +.github/actions/**/index.js +*.config.js +**/.eslintrc.js +**/node_modules/** +**/dist/** +android/**/build/** docs/vendor/** +docs/assets/** +web/gtm.js diff --git a/.eslintrc.js b/.eslintrc.js index c8a842fa4650..1f23ae22ca7e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,9 +4,9 @@ const restrictedImportPaths = [ importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text', 'ScrollView'], message: [ '', - "For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.", - "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", - "For 'StatusBar', please use 'src/libs/StatusBar' instead.", + "For 'useWindowDimensions', please use '@src/hooks/useWindowDimensions' instead.", + "For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", + "For 'StatusBar', please use '@src/libs/StatusBar' instead.", "For 'Text', please use '@components/Text' instead.", "For 'ScrollView', please use '@components/ScrollView' instead.", ].join('\n'), @@ -14,7 +14,7 @@ const restrictedImportPaths = [ { name: 'react-native-gesture-handler', importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], - message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", + message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from '@components/Pressable' instead.", }, { name: 'awesome-phonenumber', @@ -24,7 +24,7 @@ const restrictedImportPaths = [ { name: 'react-native-safe-area-context', importNames: ['useSafeAreaInsets', 'SafeAreaConsumer', 'SafeAreaInsetsContext'], - message: "Please use 'useSafeAreaInsets' from 'src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from 'src/components/SafeAreaConsumer' instead.", + message: "Please use 'useSafeAreaInsets' from '@src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from '@components/SafeAreaConsumer' instead.", }, { name: 'react', @@ -34,18 +34,18 @@ const restrictedImportPaths = [ { name: '@styles/index', importNames: ['default', 'defaultStyles'], - message: 'Do not import styles directly. Please use the `useThemeStyles` hook or `withThemeStyles` HOC instead.', + message: 'Do not import styles directly. Please use the `useThemeStyles` hook instead.', }, { name: '@styles/utils', importNames: ['default', 'DefaultStyleUtils'], - message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + message: 'Do not import StyleUtils directly. Please use the `useStyleUtils` hook instead.', }, { name: '@styles/theme', importNames: ['default', 'defaultTheme'], - message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + message: 'Do not import themes directly. Please use the `useTheme` hook instead.', }, { name: '@styles/theme/illustrations', @@ -60,15 +60,15 @@ const restrictedImportPaths = [ const restrictedImportPatterns = [ { group: ['**/assets/animations/**/*.json'], - message: "Do not import animations directly. Please use the 'src/components/LottieAnimations' import instead.", + message: "Do not import animations directly. Please use the '@components/LottieAnimations' import instead.", }, { group: ['@styles/theme/themes/**'], - message: 'Do not import themes directly. Please use the `useTheme` hook or `withTheme` HOC instead.', + message: 'Do not import themes directly. Please use the `useTheme` hook instead.', }, { group: ['@styles/utils/**', '!@styles/utils/FontUtils', '!@styles/utils/types'], - message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook or `withStyleUtils` HOC instead.', + message: 'Do not import style util functions directly. Please use the `useStyleUtils` hook instead.', }, { group: ['@styles/theme/illustrations/themes/**'], @@ -77,218 +77,175 @@ const restrictedImportPatterns = [ ]; module.exports = { - extends: ['expensify', 'plugin:storybook/recommended', 'plugin:react-native-a11y/basic', 'plugin:@dword-design/import-alias/recommended', 'prettier'], - plugins: ['react-native-a11y', 'testing-library'], - parser: 'babel-eslint', - ignorePatterns: ['!.*', 'src/vendor', '.github/actions/**/index.js', 'desktop/dist/*.js', 'dist/*.js', 'node_modules/.bin/**', 'node_modules/.cache/**', '.git/**'], + extends: [ + 'expensify', + 'airbnb-typescript', + 'plugin:storybook/recommended', + 'plugin:react-native-a11y/basic', + 'plugin:@dword-design/import-alias/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:you-dont-need-lodash-underscore/all', + 'prettier', + ], + plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + }, env: { jest: true, }, globals: { __DEV__: 'readonly', }, - overrides: [ - { - files: ['*.js', '*.jsx', '*.ts', '*.tsx'], - plugins: ['react'], - rules: { - 'prefer-regex-literals': 'off', - 'rulesdir/no-multiple-onyx-in-file': 'off', - 'react-native-a11y/has-accessibility-hint': ['off'], - 'react/jsx-no-constructed-context-values': 'error', - 'react-native-a11y/has-valid-accessibility-descriptors': [ - 'error', - { - touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'], - }, - ], - '@dword-design/import-alias/prefer-alias': [ - 'warn', - { - alias: { - '@assets': './assets', - '@components': './src/components', - '@hooks': './src/hooks', - // This is needed up here, if not @libs/actions would take the priority - '@userActions': './src/libs/actions', - '@libs': './src/libs', - '@navigation': './src/libs/Navigation', - '@pages': './src/pages', - '@styles': './src/styles', - // This path is provide alias for files like `ONYXKEYS` and `CONST`. - '@src': './src', - '@desktop': './desktop', - '@github': './.github', - }, - }, - ], - 'rulesdir/avoid-anonymous-functions': 'off', + rules: { + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + + // TypeScript specific rules + '@typescript-eslint/prefer-enum-initializers': 'error', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-non-null-assertion': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: ['variable', 'property'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], }, - }, - // This helps disable the `prefer-alias` rule to be enabled for specific directories - { - files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'assets/**/*.js', '.storybook/**/*.js'], - rules: {'@dword-design/import-alias/prefer-alias': ['off']}, - }, - { - files: ['tests/**/*.js', 'tests/**/*.ts', 'tests/**/*.jsx', 'tests/**/*.tsx'], - extends: ['plugin:testing-library/react'], - rules: { - 'testing-library/await-async-queries': 'error', - 'testing-library/await-async-utils': 'error', - 'testing-library/no-debugging-utils': 'error', - 'testing-library/no-manual-cleanup': 'error', - 'testing-library/no-unnecessary-act': 'error', - 'testing-library/prefer-find-by': 'error', - 'testing-library/prefer-presence-queries': 'error', - 'testing-library/prefer-screen-queries': 'error', + { + selector: 'function', + format: ['camelCase', 'PascalCase'], }, - }, - { - files: ['*.js', '*.jsx'], - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.website.js', '.desktop.js', '.native.js', '.ios.js', '.android.js', '.config.js', '.ts', '.tsx'], - }, + { + selector: ['typeLike', 'enumMember'], + format: ['PascalCase'], + }, + { + selector: ['parameter', 'method'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allow', + }, + ], + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + object: "Use 'Record' instead.", }, + extendDefaults: true, }, - rules: { - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: restrictedImportPaths, - patterns: restrictedImportPatterns, - }, - ], - curly: 'error', - 'react/display-name': 'error', + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + fixStyle: 'separate-type-imports', }, - }, - { - files: ['*.ts', '*.tsx'], - extends: [ - 'airbnb-typescript', - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:you-dont-need-lodash-underscore/all', - 'prettier', - ], - plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore'], - parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json', + ], + '@typescript-eslint/consistent-type-exports': [ + 'error', + { + fixMixedExportsWithInlineTypeSpecifier: false, }, - rules: { - // TODO: Remove the following rules after TypeScript migration is complete. - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', + ], + + // ESLint core rules + 'es/no-nullish-coalescing-operators': 'off', + 'es/no-optional-chaining': 'off', + + // Import specific rules + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/no-extraneous-dependencies': 'off', + + // Rulesdir specific rules + 'rulesdir/no-default-props': 'error', + 'rulesdir/no-multiple-onyx-in-file': 'off', + 'rulesdir/prefer-underscore-method': 'off', + 'rulesdir/prefer-import-module-contents': 'off', - '@typescript-eslint/naming-convention': [ - 'error', - { - selector: ['variable', 'property'], - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - }, - { - selector: 'function', - format: ['camelCase', 'PascalCase'], - }, - { - selector: ['typeLike', 'enumMember'], - format: ['PascalCase'], - }, - { - selector: ['parameter', 'method'], - format: ['camelCase', 'PascalCase'], - leadingUnderscore: 'allow', - }, - ], - '@typescript-eslint/ban-types': [ - 'error', - { - types: { - object: "Use 'Record' instead.", - }, - extendDefaults: true, - }, - ], - '@typescript-eslint/array-type': ['error', {default: 'array-simple'}], - '@typescript-eslint/prefer-enum-initializers': 'error', - '@typescript-eslint/no-var-requires': 'off', - '@typescript-eslint/no-non-null-assertion': 'error', - '@typescript-eslint/switch-exhaustiveness-check': 'error', - '@typescript-eslint/consistent-type-definitions': ['error', 'type'], - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/consistent-type-imports': [ - 'error', - { - prefer: 'type-imports', - fixStyle: 'separate-type-imports', - }, - ], - '@typescript-eslint/no-import-type-side-effects': 'error', - '@typescript-eslint/consistent-type-exports': [ - 'error', - { - fixMixedExportsWithInlineTypeSpecifier: false, - }, - ], - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'es/no-nullish-coalescing-operators': 'off', - 'es/no-optional-chaining': 'off', - 'valid-jsdoc': 'off', - 'jsdoc/no-types': 'error', - 'rulesdir/no-default-props': 'error', - 'import/no-extraneous-dependencies': 'off', - 'rulesdir/prefer-underscore-method': 'off', - 'rulesdir/prefer-import-module-contents': 'off', - 'react/require-default-props': 'off', - 'react/prop-types': 'off', - 'no-restricted-syntax': [ - 'error', - { - selector: 'TSEnumDeclaration', - message: "Please don't declare enums, use union types instead.", - }, - ], - 'no-restricted-properties': [ - 'error', - { - object: 'Image', - property: 'getSize', - message: 'Usage of Image.getImage is restricted. Please use the `react-native-image-size`.', - }, - ], - 'no-restricted-imports': [ - 'error', - { - paths: restrictedImportPaths, - patterns: restrictedImportPatterns, - }, - ], - curly: 'error', - 'you-dont-need-lodash-underscore/throttle': 'off', + // React and React Native specific rules + 'react-native-a11y/has-accessibility-hint': ['off'], + 'react/require-default-props': 'off', + 'react/prop-types': 'off', + 'react/jsx-no-constructed-context-values': 'error', + 'react-native-a11y/has-valid-accessibility-descriptors': [ + 'error', + { + touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'], }, - }, + ], + + // Disallow usage of certain functions and imports + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration', + message: "Please don't declare enums, use union types instead.", + }, + ], + 'no-restricted-properties': [ + 'error', + { + object: 'Image', + property: 'getSize', + message: 'Usage of Image.getImage is restricted. Please use the `react-native-image-size`.', + }, + ], + 'no-restricted-imports': [ + 'error', + { + paths: restrictedImportPaths, + patterns: restrictedImportPatterns, + }, + ], + + // Other rules + curly: 'error', + 'you-dont-need-lodash-underscore/throttle': 'off', + 'prefer-regex-literals': 'off', + 'valid-jsdoc': 'off', + 'jsdoc/no-types': 'error', + '@dword-design/import-alias/prefer-alias': [ + 'warn', + { + alias: { + '@assets': './assets', + '@components': './src/components', + '@hooks': './src/hooks', + // This is needed up here, if not @libs/actions would take the priority + '@userActions': './src/libs/actions', + '@libs': './src/libs', + '@navigation': './src/libs/Navigation', + '@pages': './src/pages', + '@styles': './src/styles', + // This path is provide alias for files like `ONYXKEYS` and `CONST`. + '@src': './src', + '@desktop': './desktop', + '@github': './.github', + }, + }, + ], + }, + + // Remove once no JS files are left + overrides: [ { - files: ['workflow_tests/**/*.{js,jsx,ts,tsx}', 'tests/**/*.{js,jsx,ts,tsx}', '.github/**/*.{js,jsx,ts,tsx}'], + files: ['*.js', '*.jsx'], rules: { - '@lwc/lwc/no-async-await': 'off', - 'no-await-in-loop': 'off', - 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/unbound-method': 'off', + 'jsdoc/no-types': 'off', + 'react/jsx-filename-extension': 'off', + 'rulesdir/no-default-props': 'off', }, }, { diff --git a/.github/.eslintrc.js b/.github/.eslintrc.js index e769944cd1a9..d6d39822b737 100644 --- a/.github/.eslintrc.js +++ b/.github/.eslintrc.js @@ -1,6 +1,10 @@ -// For all these Node.js scripts, we do not want to disable `console` statements module.exports = { rules: { + // For all these Node.js scripts, we do not want to disable `console` statements 'no-console': 'off', + + '@lwc/lwc/no-async-await': 'off', + 'no-await-in-loop': 'off', + 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }; diff --git a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts index 26947193cd80..96bb17a14354 100644 --- a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts +++ b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts @@ -8,19 +8,10 @@ import {promiseDoWhile} from '@github/libs/promiseWhile'; type CurrentStagingDeploys = Awaited>['data']['workflow_runs']; function run() { - console.info('[awaitStagingDeploys] POLL RATE', CONST.POLL_RATE); - console.info('[awaitStagingDeploys] run()'); - console.info('[awaitStagingDeploys] getStringInput', getStringInput); - console.info('[awaitStagingDeploys] GitHubUtils', GitHubUtils); - console.info('[awaitStagingDeploys] promiseDoWhile', promiseDoWhile); - const tag = getStringInput('TAG', {required: false}); - console.info('[awaitStagingDeploys] run() tag', tag); let currentStagingDeploys: CurrentStagingDeploys = []; - console.info('[awaitStagingDeploys] run() _.throttle', lodashThrottle); - const throttleFunc = () => Promise.all([ // These are active deploys @@ -42,24 +33,20 @@ function run() { }), ]) .then((responses) => { - console.info('[awaitStagingDeploys] listWorkflowRuns responses', responses); const workflowRuns = responses[0].data.workflow_runs; if (!tag && typeof responses[1] === 'object') { workflowRuns.push(...responses[1].data.workflow_runs); } - console.info('[awaitStagingDeploys] workflowRuns', workflowRuns); return workflowRuns; }) .then((workflowRuns) => (currentStagingDeploys = workflowRuns.filter((workflowRun) => workflowRun.status !== 'completed'))) .then(() => { - console.info('[awaitStagingDeploys] currentStagingDeploys', currentStagingDeploys); console.log( !currentStagingDeploys.length ? 'No current staging deploys found' : `Found ${currentStagingDeploys.length} staging deploy${currentStagingDeploys.length > 1 ? 's' : ''} still running...`, ); }); - console.info('[awaitStagingDeploys] run() throttleFunc', throttleFunc); return promiseDoWhile( () => !!currentStagingDeploys.length, diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index c91313520673..0e0168fdb7ae 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12131,15 +12131,8 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); const promiseWhile_1 = __nccwpck_require__(9438); function run() { - console.info('[awaitStagingDeploys] POLL RATE', CONST_1.default.POLL_RATE); - console.info('[awaitStagingDeploys] run()'); - console.info('[awaitStagingDeploys] getStringInput', ActionUtils_1.getStringInput); - console.info('[awaitStagingDeploys] GitHubUtils', GithubUtils_1.default); - console.info('[awaitStagingDeploys] promiseDoWhile', promiseWhile_1.promiseDoWhile); const tag = (0, ActionUtils_1.getStringInput)('TAG', { required: false }); - console.info('[awaitStagingDeploys] run() tag', tag); let currentStagingDeploys = []; - console.info('[awaitStagingDeploys] run() _.throttle', throttle_1.default); const throttleFunc = () => Promise.all([ // These are active deploys GithubUtils_1.default.octokit.actions.listWorkflowRuns({ @@ -12159,22 +12152,18 @@ function run() { }), ]) .then((responses) => { - console.info('[awaitStagingDeploys] listWorkflowRuns responses', responses); const workflowRuns = responses[0].data.workflow_runs; if (!tag && typeof responses[1] === 'object') { workflowRuns.push(...responses[1].data.workflow_runs); } - console.info('[awaitStagingDeploys] workflowRuns', workflowRuns); return workflowRuns; }) .then((workflowRuns) => (currentStagingDeploys = workflowRuns.filter((workflowRun) => workflowRun.status !== 'completed'))) .then(() => { - console.info('[awaitStagingDeploys] currentStagingDeploys', currentStagingDeploys); console.log(!currentStagingDeploys.length ? 'No current staging deploys found' : `Found ${currentStagingDeploys.length} staging deploy${currentStagingDeploys.length > 1 ? 's' : ''} still running...`); }); - console.info('[awaitStagingDeploys] run() throttleFunc', throttleFunc); return (0, promiseWhile_1.promiseDoWhile)(() => !!currentStagingDeploys.length, (0, throttle_1.default)(throttleFunc, // Poll every 60 seconds instead of every 10 seconds CONST_1.default.POLL_RATE * 6)); @@ -12730,7 +12719,6 @@ exports.promiseDoWhile = exports.promiseWhile = void 0; * Simulates a while loop where the condition is determined by the result of a Promise. */ function promiseWhile(condition, action) { - console.info('[promiseWhile] promiseWhile()'); return new Promise((resolve, reject) => { const loop = function () { if (!condition()) { @@ -12738,7 +12726,6 @@ function promiseWhile(condition, action) { } else { const actionResult = action?.(); - console.info('[promiseWhile] promiseWhile() actionResult', actionResult); if (!actionResult) { resolve(); return; @@ -12759,11 +12746,8 @@ exports.promiseWhile = promiseWhile; * Simulates a do-while loop where the condition is determined by the result of a Promise. */ function promiseDoWhile(condition, action) { - console.info('[promiseWhile] promiseDoWhile()'); return new Promise((resolve, reject) => { - console.info('[promiseWhile] promiseDoWhile() condition', condition); const actionResult = action?.(); - console.info('[promiseWhile] promiseDoWhile() actionResult', actionResult); if (!actionResult) { resolve(); return; diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts index 401b6ee2e18a..8bedceb894fd 100644 --- a/.github/libs/promiseWhile.ts +++ b/.github/libs/promiseWhile.ts @@ -4,15 +4,12 @@ import type {DebouncedFunc} from 'lodash'; * Simulates a while loop where the condition is determined by the result of a Promise. */ function promiseWhile(condition: () => boolean, action: (() => Promise) | DebouncedFunc<() => Promise> | undefined): Promise { - console.info('[promiseWhile] promiseWhile()'); - return new Promise((resolve, reject) => { const loop = function () { if (!condition()) { resolve(); } else { const actionResult = action?.(); - console.info('[promiseWhile] promiseWhile() actionResult', actionResult); if (!actionResult) { resolve(); @@ -35,12 +32,9 @@ function promiseWhile(condition: () => boolean, action: (() => Promise) | * Simulates a do-while loop where the condition is determined by the result of a Promise. */ function promiseDoWhile(condition: () => boolean, action: (() => Promise) | DebouncedFunc<() => Promise> | undefined): Promise { - console.info('[promiseWhile] promiseDoWhile()'); - return new Promise((resolve, reject) => { - console.info('[promiseWhile] promiseDoWhile() condition', condition); const actionResult = action?.(); - console.info('[promiseWhile] promiseDoWhile() actionResult', actionResult); + if (!actionResult) { resolve(); return; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71b4bc3d8fc3..c76425a40fbf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: key: ${{ runner.os }}-jest - name: Jest tests - run: npx jest --silent --shard=${{ fromJSON(matrix.chunk) }}/${{ strategy.job-total }} --max-workers ${{ steps.cpu-cores.outputs.count }} + run: NODE_OPTIONS="$NODE_OPTIONS --experimental-vm-modules" npx jest --silent --shard=${{ fromJSON(matrix.chunk) }}/${{ strategy.job-total }} --max-workers ${{ steps.cpu-cores.outputs.count }} storybookTests: if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} diff --git a/.nvmrc b/.nvmrc index d5a159609d09..62d44807d084 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.10.0 +20.13.0 diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index ec8e17dda4cf..7527857eeda7 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -16,7 +16,7 @@ import './fonts.css'; Onyx.init({ keys: ONYXKEYS, initialKeyStates: { - [ONYXKEYS.NETWORK]: {isOffline: false}, + [ONYXKEYS.NETWORK]: {isOffline: false, isBackendReachable: true}, }, }); diff --git a/android/app/build.gradle b/android/app/build.gradle index 7f8a1c615078..ed0ec6571b9d 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 1001047201 - versionName "1.4.72-1" + versionCode 1001047307 + versionName "1.4.73-7" // 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/assets/emojis/index.ts b/assets/emojis/index.ts index 02328001674e..d230e8eec2be 100644 --- a/assets/emojis/index.ts +++ b/assets/emojis/index.ts @@ -1,7 +1,5 @@ import type {Locale} from '@src/types/onyx'; import emojis from './common'; -import enEmojis from './en'; -import esEmojis from './es'; import type {Emoji, EmojisList} from './types'; type EmojiTable = Record; @@ -30,10 +28,22 @@ const emojiCodeTableWithSkinTones = emojis.reduce((prev, cur) => { }, {}); const localeEmojis: LocaleEmojis = { - en: enEmojis, - es: esEmojis, + en: undefined, + es: undefined, +}; + +const importEmojiLocale = (locale: Locale) => { + const normalizedLocale = locale.toLowerCase().split('-')[0] as Locale; + if (!localeEmojis[normalizedLocale]) { + const emojiImportPromise = normalizedLocale === 'en' ? import('./en') : import('./es'); + return emojiImportPromise.then((esEmojiModule) => { + // it is needed because in jest test the modules are imported in double nested default object + localeEmojis[normalizedLocale] = esEmojiModule.default.default ? (esEmojiModule.default.default as unknown as EmojisList) : esEmojiModule.default; + }); + } + return Promise.resolve(); }; export default emojis; -export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; +export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis, importEmojiLocale}; export {skinTones, categoryFrequentlyUsed} from './common'; diff --git a/assets/images/credit-card-hourglass.svg b/assets/images/credit-card-hourglass.svg new file mode 100644 index 000000000000..2acd013fbe59 --- /dev/null +++ b/assets/images/credit-card-hourglass.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg index ecdf3cf2e115..c93986de3c9b 100644 --- a/assets/images/receipt-scan.svg +++ b/assets/images/receipt-scan.svg @@ -1,5 +1,5 @@ - + ` 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/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md new file mode 100644 index 000000000000..6bc3b0896912 --- /dev/null +++ b/docs/articles/new-expensify/connections/Set-up-QuickBooks-Online-connection.md @@ -0,0 +1,126 @@ +--- +title: Set up QuickBooks Online connection +description: Integrate QuickBooks Online with Expensify +--- +
+ +{% include info.html %} +To use the QuickBooks Online connection, you must have a QuickBooks Online account and an Expensify Collect plan. The QuickBooks Self-employed subscription is not supported. +{% include end-info.html %} + +The features available for the Expensify connection with QuickBooks Online vary based on your QuickBooks subscription. The features may still be visible in Expensify even if you don’t have access, but you will receive an error if the feature isn't available with your subscription. + +Here is a list of the features supported by each QuickBooks Online subscription: + +| Feature | Simple Start | Essentials | Essentials Plus | +|----------------------------|--------------|------------|-----------------| +| Expense Reports | ✔ | ✔ | ✔ | +| GL Accounts as Categories | ✔ | ✔ | ✔ | +| Credit Card Transactions | ✔ | ✔ | ✔ | +| Debit Card Transaction | | ✔ | ✔ | +| Classes | | ✔ | ✔ | +| Customers | | ✔ | ✔ | +| Projects | | ✔ | ✔ | +| Vendor Bills | | ✔ | ✔ | +| Journal Entries | | ✔ | ✔ | +| Tax | | ✔ | ✔ | +| Billable | | | ✔ | +| Location | | | ✔ | + +To set up your QuickBooks Online connection, complete the 5 steps below. + +# Step 1: Set up employees in QuickBooks Online + +Log in to QuickBooks Online and ensure all of your employees are setup as either Vendors or Employees using the same email address that they are listed under in Expensify. This process may vary by country, but you can go to **Payroll** and select **Employees** in QuickBooks Online to add new employees or edit existing ones. + +# Step 2: Connect Expensify to QuickBooks Online + +
    +
  1. Click your profile image or icon in the bottom left menu.
  2. +
  3. Scroll down and click Workspaces in the left menu.
  4. +
  5. Select the workspace you want to connect to QuickBooks Online.
  6. +
  7. Click More features in the left menu.
  8. +
  9. Scroll down to the Integrate section and enable the Accounting toggle.
  10. +
  11. Click Accounting in the left menu.
  12. +
  13. Click Set up to the right of QuickBooks Online.
  14. +
  15. Enter your Intuit login details to import your settings from QuickBooks Online to Expensify.
  16. +
+ +# Step 3: Configure import settings + +The following steps help you determine how data will be imported from QuickBooks Online to Expensify. + +
    +
  1. Under the Accounting settings for your workspace, click Import under the QuickBooks Online connection.
  2. +
  3. Review each of the following import settings:
  4. +
      +
    • Chart of accounts: The chart of accounts are automatically imported from QuickBooks Online as categories. This cannot be amended.
    • +
    • Classes: Choose whether to import classes, which will be shown in Expensify as tags for expense-level coding.
    • +
    • Customers/projects: Choose whether to import customers/projects, which will be shown in Expensify as tags for expense-level coding.
    • +
    • Locations: Choose whether to import locations, which will be shown in Expensify as tags for expense-level coding.
    • +{% include info.html %} +As Locations are only configurable as tags, you cannot export expense reports as vendor bills or checks to QuickBooks Online. To unlock these export options, either disable locations import or upgrade to the Control Plan to export locations encoded as a report field. +{% include end-info.html %} +
    • Taxes: Choose whether to import tax rates and defaults.
    • +
    +
+ +# Step 4: Configure export settings + +The following steps help you determine how data will be exported from Expensify to QuickBooks Online. + +
    +
  1. Under the Accounting settings for your workspace, click Export under the QuickBooks Online connection.
  2. +
  3. Review each of the following export settings:
  4. +
      +
    • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
    • + +{% include info.html %} +* Other Workspace Admins will still be able to export to QuickBooks Online. +* If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. +{% include end-info.html %} + +
    • Date: Choose whether to use the date of last expense, export date, or submitted date.
    • +
    • Export Out-of-Pocket Expenses as: Select whether out-of-pocket expenses will be exported as a check, journal entry, or vendor bill.
    • + +{% include info.html %} +These settings may vary based on whether tax is enabled for your workspace. +* If tax is not enabled on the workspace, you’ll also select the Accounts Payable/AP. +* If tax is enabled on the workspace, journal entry will not be available as an option. If you select the journal entries option first and later enable tax on the workspace, you will see a red dot and an error message under the “Export Out-of-Pocket Expenses as” options. To resolve this error, you must change your export option to vendor bill or check to successfully code and export expense reports. +{% include end-info.html %} + +
    • Invoices: Select the QuickBooks Online invoice account that invoices will be exported to.
    • +
    • Export as: Select whether company cards export to QuickBooks Online as a credit card (the default), debit card, or vendor bill. Then select the account they will export to.
    • +
    • If you select vendor bill, you’ll also select the accounts payable account that vendor bills will be created from, as well as whether to set a default vendor for credit card transactions upon export. If this option is enabled, you will select the vendor that all credit card transactions will be applied to.
    • +
    +
+ +# Step 5: Configure advanced settings + +The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings. + +
    +
  1. Under the Accounting settings for your workspace, click Advanced under the QuickBooks Online connection.
  2. +
  3. Select an option for each of the following settings:
  4. +
      +
    • Auto-sync: Choose whether to enable QuickBooks Online to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period.
    • +
    • Invite Employees: Choose whether to enable Expensify to import employee records from QuickBooks Online and invite them to this workspace.
    • +
    • Automatically Create Entities: Choose whether to enable Expensify to automatically create vendors and customers in QuickBooks Online if a matching vendor or customer does not exist.
    • +
    • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in QuickBooks Online will also show in Expensify as Paid. If enabled, you must also select the QuickBooks Online account that reimbursements are coming out of, and Expensify will automatically create the payment in QuickBooks Online.
    • +
    • Invoice Collection Account: Select the invoice collection account that you want invoices to appear under once the invoice is marked as paid.
    • +
    +
+ +{% include faq-begin.md %} + +**Why do I see a red dot next to my connection?** +If there is an error with your connection, you’ll see a red dot next to Accounting in the left menu. When you click Accounting, you’ll also see a red dot displayed next to the QuickBooks Online connection card. + +This may occur if you incorrectly enter your QuickBooks Online login information when trying to establish the connection. To resubmit your login details, +1. Click the three-dot menu to the right of the QuickBooks Online connection. +2. Click **Enter credentials**. +3. Enter your Intuit login details (the login information you use for QuickBooks Online) to establish the connection. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md b/docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md new file mode 100644 index 000000000000..ace488f589a1 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Resolve-Errors-Adding-a-Bank-Account.md @@ -0,0 +1,21 @@ +--- +title: Resolve Errors Adding a Bank Account +description: Troubleshooting issues adding a business bank account in Expensify. +--- +
+ +Expensify is required to verify the identity of the individual who is connecting a business bank account. + +**If you get a generic error message while uploading your ID, please go through the following steps:** +1. Ensure you are using either Safari (on iPhone) or Chrome (on Android) as your web browser. +2. Check your browser's permissions to make sure that the camera and microphone settings are set to "Allow" +3. Clear your web cache for Safari (on iPhone) or Chrome (on Android). +4. If using a corporate Wi-Fi network, confirm that your corporate firewall isn't blocking the website. +5. Make sure no other apps are overlapping your screen, such as the Facebook Messenger bubble, while recording the video. +6. On iPhone, if using iOS version 15 or later, disable the Hide IP address feature in Safari. +7. If possible, try these steps on another device +8. If you have another phone available, try to follow these steps on that device + +If the issue persists, please contact your Account Manager or Concierge for further troubleshooting assistance. + +
diff --git a/docs/articles/new-expensify/expenses/Track-expenses.md b/docs/articles/new-expensify/expenses/Track-expenses.md new file mode 100644 index 000000000000..f4eeea09ecec --- /dev/null +++ b/docs/articles/new-expensify/expenses/Track-expenses.md @@ -0,0 +1,43 @@ +--- +title: Track Expenses +description: Create, store, or share non-reimbursable expenses +--- +
+ +Create, store, or share non-reimbursable expenses with the Track Expenses feature. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Track Expense**. +2. Create the expense manually, scan the receipt, or add a distance expense. + +{% include info.html %} +For an in-depth walkthrough on how to create an expense, check out the create an expense article. +{% include end-info.html %} + +3. Choose the next steps for the expense: + - **Submit it to someone**: Select this option to request payment from other members of your Expensify workspace. + - **Categorize it**: Select this option to choose a category and additional details to code the expense for a specific workspace. The expense will then be placed on a report and can be submitted to the workspace for approval. + - **Share it with my accountant**: Select this option to share the expense with your accountant. The expense will then be placed on a report under the workspace for your accountant to review. + - **Nothing for now**: Select this option to store the expense. Expensify will keep the expense until you are ready to take action on it—it won’t expire. When you’re ready, you can then select one of the above options for the expense at a later time. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom menu and select **Track Expense**. +2. Create the expense manually, scan the receipt, or add a distance expense. + +{% include info.html %} +For an in-depth walkthrough on how to create an expense, check out the create an expense article. +{% include end-info.html %} + +3. Choose the next steps for the expense: + - **Submit it to someone**: Select this option to request payment from a contact in your phone’s contact list or from other members of your Expensify workspace. + - **Categorize it**: Select this option to choose a category and additional details to code the expense for a specific workspace. The expense will then be placed on a report and can be submitted to the workspace for approval. + - **Share it with my accountant**: Select this option to share the expense with your accountant. The expense will then be placed on a report under the workspace for your accountant to review. + - **Nothing for now**: Select this option to store the expense. Expensify will keep the expense until you are ready to take action on it—it won’t expire. When you’re ready, you can then select one of the above options for the expense at a later time. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md new file mode 100644 index 000000000000..24f178db9f12 --- /dev/null +++ b/docs/articles/new-expensify/expensify-card/Upgrade-to-the-new-Expensify-Card-from-Visa.md @@ -0,0 +1,54 @@ +--- +title: Upgrade to the new Expensify Card from Visa +description: Get the new Expensify Visa® Commercial Card +--- +
+ +If your company is already using Expensify Cards, you can upgrade your cards for free to the new Expensify Visa® Commercial Card to get even more tools to manage employee spending, including: +- Unlimited virtual cards +- Controlled spending amounts on virtual cards to manage subscriptions +- Tighter controls for managing spend across employees and merchants +- Fixed or monthly spend limits for each card +- Unique naming for each virtual card for simplified expense categorization + +# Upgrade your company’s Expensify Cards + +{% include info.html %} +This process must be completed by a Domain Admin. Although the process is available for all Domain Admins, only one admin needs to complete these steps. + +Before completing this process, you’ll want to: + +- Have your employees update their address if needed so that they receive their new Expensify Card in the mail before completing the steps below. +- Ensure that existing cardholders have a limit greater than $0 if you want them to receive a new Expensify Card. If their limit is $0, increase the limit. +{% include end-info.html %} + +1. On your Home page, click the task titled “Upgrade to the new and improved Expensify Card.” +2. Review and agree to the Terms of Service. +3. Click **Get the new card**. All existing cardholders with a limit greater than $0 will be automatically mailed a new physical card to the address they have on file. Virtual cards will be automatically issued and available for immediate use. +4. If you have Positive Pay enabled for your settlement account, contact your bank as soon as possible to whitelist the new ACH ID: 2270239450. +5. Remind your employees to update their payment information for recurring charges to their virtual card information. + +New cards will have the same limit as the existing cards. Each cardholder’s current physical and virtual cards will remain active until a Domain Admin or the cardholder deactivates it. + +{% include info.html %} +Cards won’t be issued to any employees who don’t currently have them. In this case, you’ll need to issue a new card. +{% include end-info.html %} + +{% include faq-begin.md %} + +**Why don’t I see the task to agree to new terms on my Home page?** + +There are a few reasons why you might not see the task on your Home page: +- You may not be a Domain Admin +- Another domain admin has already accepted the terms +- The task may be hidden. To find hidden tasks, scroll to the bottom of the Home page and click **Show Hidden Tasks** to see all of your available tasks. + +**Will this affect the continuous reconciliation process?** + +No. During the transition period, you may have some employees with old cards and some with new cards, so you’ll have two different debits (settlements) made to your settlement account for each settlement period. Once all spending has transitioned to the new cards, you’ll only see one debit/settlement. + +**Do I have to upgrade to the new Expensify Visa® Commercial Card?** + +Yes. We’ll provide a deadline soon. But don’t worry—you’ll have plenty of time to upgrade. +{% include faq-end.md %} +
diff --git a/docs/articles/new-expensify/settings/Close-account.md b/docs/articles/new-expensify/settings/Close-account.md new file mode 100644 index 000000000000..e0d8fba2f452 --- /dev/null +++ b/docs/articles/new-expensify/settings/Close-account.md @@ -0,0 +1,44 @@ +--- +title: Close account +description: Close an Expensify account +--- +
+ +Closing your account will delete the data associated with the account. However, transactions shared with other accounts, including approved and reimbursed company expenses, will still be visible under those accounts. We may also be required to retain certain transaction records in compliance with laws in various jurisdictions. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Security** in the left menu. +3. Click **Close account**. +4. Provide answers to the question prompts, then click **Close Account**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Security**. +3. Tap **Close account**. +4. Provide answers to the question prompts, then tap **Close Account**. +{% include end-option.html %} + +{% include end-selector.html %} + +{% include faq-begin.md %} + +**I’m unable to close my account.** + +If your account has an outstanding balance or if you have been assigned a role under a company’s Expensify workspace, you may encounter an error message during the account closure process, or the Close Account button may not be available. Here are the steps to follow for each scenario: + +- **Account Under a Validated Domain**: A Domain Admin must remove your account from the domain. Then you will be able to successfully close your account. +- **Sole Domain Admin**: If you are the only Domain Admin for a company’s domain, you must assign a new Domain Admin before you can close your account. +- **Workspace Billing Owner with an annual subscription**: You must downgrade from the annual subscription before closing the account. Alternatively, you can have another user take over billing for your workspaces. +- **Company Workspace Owner**: You must assign a new workspace owner before you can close your account. +- **Account has an outstanding balance**: You must make a payment to resolve the outstanding balance before you can close your account. +- **Preferred Exporter for a workspace integration**: You must assign a new Preferred Exporter before closing your account. +- **Verified Business Account that is locked**: You must unlock the account. +- **Verified Business Account that has an outstanding balance**: You must make a payment to settle any outstanding balances before the account can be closed. +- **Unverified account**: You must first verify your account before it can be closed. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/settings/Security.md b/docs/articles/new-expensify/settings/Encryption-and-Data-Security.md similarity index 99% rename from docs/articles/new-expensify/settings/Security.md rename to docs/articles/new-expensify/settings/Encryption-and-Data-Security.md index 5c8eee7ae60e..fff3e6365ff9 100644 --- a/docs/articles/new-expensify/settings/Security.md +++ b/docs/articles/new-expensify/settings/Encryption-and-Data-Security.md @@ -1,5 +1,5 @@ --- -title: Security +title: Encryption and Data Security description: Expensify prioritizes data security and maintains strict compliance standards to safeguard users' sensitive information. --- diff --git a/docs/articles/new-expensify/settings/Profile.md b/docs/articles/new-expensify/settings/Profile.md deleted file mode 100644 index 908cf39c7ac6..000000000000 --- a/docs/articles/new-expensify/settings/Profile.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Profile -description: How to manage your Expensify Profile ---- -# Overview -Your Profile in Expensify allows you to: -- Set your public profile photo -- Set a display name -- Manage your contact methods -- Communicate your current status -- Set your pronouns -- Configure your timezone -- Store your personal details (for travel and payment purposes) - -# How to set your public profile photo - -To set or update your profile photo: -1. Go to **Settings > Profile** -2. Tap on the default or your existing profile photo, -3. You can either either upload (to set a new profile photo), remove or view your profile photo - -Your profile photo is visible to all Expensify users. - -# How to set a display name - -To set or update your display name: -1. Go to **Settings > Profile** -2. Tap on **Display name** -3. Set a first name and a last name, then **Save** - -Your display name is public to all Expensify users. - -# How to add or remove contact methods (email address and phone number) - -Your contact methods allow people to contact you (using your email address or phone number), and allow you to forward receipts to receipts@expensify.com from multiple email addresses. - -To manage your contact methods: -1. Go to **Settings > Profile** -2. Tap on **Contact method** -3. Tap **New contact method** to add a new email or phone number - -Your default contact method (email address or phone number) will be visible to "known" users, with whom you have interacted or are part of your team. - -To change the email address or phone number that's displayed on your Expensify account, add a new contact method, then tap on that email address and tap **Set as default**. - -# How to communicate your current status - -You can use your status emoji to communicate your mood, focus or current activity. You can optionally add a status message too! - -To set your status emoji and status message: -1. Go to **Settings > Profile** -2. Tap on **Status** then **Status** -3. Choose a status emoji, and optionally set a status message -4. Tap on **Save** - -Your status emoji will be visible next to your name in Expensify, and your status emoji and status message will appear in your profile (which is public to all Expensify users). On a computer, your status message will also be visible by hovering your mouse over your name. - -You can also remove your current status: -1. Go to **Settings > Profile** -2. Tap on **Status** -3. Tap on **Clear status** - -# How to set your pronouns - -To set your pronouns: -1. Go to **Settings > Profile** -2. Tap on **Pronouns** -3. Search for your preferred pronouns, then tap on your choice - -Your pronouns will be visible to "known" users, with whom you have interacted or are part of your team. - -# How to configure your timezone - -Your timezone is automatically set using an estimation based on your IP address. - -To set your timezone manually: -1. Go to **Settings > Profile** -2. Tap on **Timezone** -3. Disable **Automatically determine your location** -4. Tap on **Timezone** -5. Search for your preferred timezone, then tap on your choice - -Your timezone will be visible to "known" users, with whom you have interacted or are part of your team. - -# How to store your personal details (for travel and payment purposes) - -Your personal details can be used in Expensify for travel and payment purposes. These will not be shared with any other Expensify user. - -To set your timezone manually: -1. Go to **Settings > Profile** -2. Tap on **Personal details** -3. Tap on **Legal name**, **Date of birth**, and **Address** to set your personal details diff --git a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md new file mode 100644 index 000000000000..34f96f9f5f7d --- /dev/null +++ b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md @@ -0,0 +1,30 @@ +--- +title: Switch to light or dark mode +description: Change the appearance of Expensify +--- +
+ +Expensify has three theme options that determine how the app looks: +- **Dark mode**: The app appears with a dark background +- **Light mode**: The app appears with a light background +- **Use Device settings**: Expensify will automatically use your device’s default theme + +To change your Expensify theme, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click **Preferences** in the left menu. +3. Click the **Theme** option and select the desired theme. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Preferences**. +3. Tap the **Theme** option and select the desired theme. +{% include end-option.html %} + +{% include end-selector.html %} + +
diff --git a/docs/articles/new-expensify/workspaces/Coming-Soon.md b/docs/articles/new-expensify/workspaces/Coming-Soon.md new file mode 100644 index 000000000000..266784414761 --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Coming-Soon.md @@ -0,0 +1,6 @@ +--- +title: Coming Soon! +description: More info coming soon! +--- + + diff --git a/docs/articles/new-expensify/workspaces/Create-expense-categories.md b/docs/articles/new-expensify/workspaces/Create-expense-categories.md new file mode 100644 index 000000000000..7b8d29d09d1c --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Create-expense-categories.md @@ -0,0 +1,114 @@ +--- +title: Create expense categories +description: Add categories to use for coding expenses +--- +
+ +In Expensify, categories refer to the **chart of accounts, GL accounts, expense accounts**, and other line-item details that help code expenses for accounting and financial reporting. + +An admin can manually create categories for a workspace, or they will be automatically imported if your workspace is connected to another platform such as QuickBooks Online, QuickBooks Desktop, Intacct, Xero, or NetSuite. These imported categories can be enabled or disabled to use as categories for expenses added to Expensify. Additionally, Expensify will learn how you apply categories to specific merchants over time and apply them automatically. + +# Manually add or delete categories + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +To manually add a category, + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to add categories to. +4. Click **Categories** in the left menu. +5. Click **Add Category** in the top right. +6. Enter a name for the category and click **Save**. + +To delete a category, + +1. Click the category on the Categories page. +2. Click the 3-dot menu in the top right. +3. Click **Delete category** to permanently delete the category. +{% include end-option.html %} + +{% include option.html value="mobile" %} +To manually add a category, + +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select the workspace you want to add categories to. +4. Tap **Categories**. +5. Tap **Add Category**. +6. Enter a name for the category and tap **Save**. + +To delete a category, +1. Tap the category on the Categories page. +2. Tap the 3-dot menu in the top right. +3. Tap **Delete category** to permanently delete the category. +{% include end-option.html %} + +{% include end-selector.html %} + +# Enable or disable categories + +Once you have manually added your categories or automatically imported them from a connected accounting system, you can enable or disable the categories to determine whether they can be added to expenses. + +{% include info.html %} +After connecting an accounting system, Expensify automatically imports charts of accounts, GL accounts, expense accounts, and additional details into your workspace as **disabled** categories. Workspace admins can enable these categories to make them available for workspace members to add to their expenses. +{% include end-info.html %} + +To enable or disable a category, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select a workspace. +4. Click **Categories** in the left menu. +5. Click a category and use the toggle to enable or disable it. + +{% include info.html %} +You can enable, disable, or delete categories in bulk by selecting the checkbox to the left of the categories. Then click the “selected” dropdown menu at the top right of the page and select the desired option. +{% include end-info.html %} + +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select a workspace. +4. Tap **Categories**. +5. Tap a category and use the toggle to enable or disable it. + +{% include info.html %} +You can enable, disable, or delete categories in bulk by selecting the checkbox to the left of the categories. Then tap the “selected” dropdown menu at the top of the page and select the desired option. +{% include end-info.html %} + +{% include end-option.html %} + +{% include end-selector.html %} + +# Automatic Expensify categories + +Over time, Expensify learns how you categorize specific merchants and automatically applies that category to the merchant in the future. +- If you change a category, Expensify learns that correction over time as well. However, changing a category on one expense does not change it for other expenses that have already been assigned the category. +- Any expense rules for your workspace take priority over Expensify’s automatic categories. +- Expensify won’t automatically add a category to an expense if it is already manually assigned a category. + +{% include faq-begin.md %} +**Can I edit my categories on a submitted expense report?** + +Yes, you can edit categories on an expense you have submitted until the expense is approved or reimbursed. + +Approvers can also edit categories on the submitter’s behalf, even after approval. If you are an approver reviewing a report that wasn’t submitted to you, you’ll see the option to take control of the report and then you can change the category. + +**Can I see an audit trail of category changes on an expense?** + +Yes. When a category is manually edited, Expensify will log the change in the related workspace chat. + +**If I change categories in my accounting system, what happens to categories in the workspace?** + +If a category is disabled in the accounting system, it will be removed from the workspace’s categories list in the workspace. However, the disabled category will remain on approved and drafted expense reports that it has been previously added to. An admin can change the category on an approved or reimbursed expense, and anyone can change the category on an unapproved expense. + +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/workspaces/Create-expense-tags.md b/docs/articles/new-expensify/workspaces/Create-expense-tags.md new file mode 100644 index 000000000000..cf7bdd6fc6a7 --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Create-expense-tags.md @@ -0,0 +1,121 @@ +--- +title: Create expense tags +description: Add tags to use for coding expenses +--- +
+ +In Expensify, tags refer to **classes, projects, cost centers, locations, customers, jobs**, and other line-item details that help code expenses for accounting and financial reporting. + +An admin can manually create tags for a workspace, or they will be automatically imported if your workspace is connected to an accounting system, like QuickBooks Online or Xero. These imported tags can be enabled or disabled to use as tags for expenses added to Expensify. Additionally, Expensify will learn how you apply tags to specific merchants over time and apply them automatically. + +# Manually add or delete tags + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +To manually add a tag, + +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select the workspace you want to add tags to. +4. Click **More features** in the left menu. +5. Scroll down to the Organize section and enable the Tags toggle. +6. Click **Tags** in the left menu. +7. Click **Add Tag** in the top right. +8. Enter a name for the tag and click **Save**. + +To delete a tag, + +1. Click the tag on the Tags page. +2. Click the 3-dot menu in the top right. +3. Click **Delete tag** to permanently delete the tag. +{% include end-option.html %} + +{% include option.html value="mobile" %} +To manually add a tag, + +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select the workspace you want to add tags to. +4. Tap **More features**. +5. In the Organize section, enable the Tags toggle. +6. Tap **Tags**. +7. Tap **Add Tag**. +8. Enter a name for the tag and tap **Save**. + +To delete a tag, +1. Tap the tag on the Tags page. +2. Tap the 3-dot menu in the top right. +3. Tap **Delete tag** to permanently delete the tag. +{% include end-option.html %} + +{% include end-selector.html %} + +# Enable or disable tags + +Once you have manually added your tags or automatically imported them from a connected accounting system, you can enable or disable the tags to determine whether they can be added to expenses. + +{% include info.html %} +After connecting an accounting system, Expensify automatically imports classes, projects, customers, and additional details into your workspace as disabled tags. Workspace admins can enable these tags to make them available for workspace members to add to their expenses. +{% include end-info.html %} + +To enable or disable a tag, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select a workspace. +4. Click **Tags** in the left menu. +5. Click a tag and use the toggle to enable or disable it. + +{% include info.html %} +You can enable, disable, or delete tags in bulk by selecting the checkbox to the left of the tags. Then click the “selected” dropdown menu at the top right of the page and select the desired option. +{% include end-info.html %} + +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select a workspace. +4. Tap **Tags**. +5. Tap a tag and use the toggle to enable or disable it. + +{% include info.html %} +You can enable, disable, or delete tags in bulk by selecting the checkbox to the left of the tag. Then tap the “selected” dropdown menu at the top of the page and select the desired option. +{% include end-info.html %} + +{% include end-option.html %} + +{% include end-selector.html %} + +# Automatic Expensify tags + +Over time, Expensify learns how you tag specific merchants and automatically applies that tag to the merchant in the future. +- If you change a tag, Expensify learns that correction over time as well. However, changing a tag on one expense does not change it for other expenses that have already been assigned the tag. +- Any expense rules for your workspace take priority over Expensify’s automatic tags. +- Expensify won’t automatically tag an expense if it is already manually assigned a tag. + +{% include faq-begin.md %} +**Can I edit my tags on a submitted expense report?** + +Yes, you can edit tags on an expense you have submitted until the expense is approved or reimbursed. + +Approvers can also edit tags on the submitter’s behalf, even after approval. If you are an approver reviewing a report that wasn’t submitted to you, you’ll see the option to take control of the report and then you can change the tag. + +**Can I see an audit trail of tag changes on an expense?** + +Yes. When a tag is manually edited, Expensify will log the change in the related workspace chat. + +**If I change tags in my accounting system, what happens to tags in the workspace?** + +If a tag is disabled in the accounting system, it will be removed from the workspace’s tags list in the workspace. However, the disabled tag will remain on approved and drafted expense reports that it has been previously added to. An admin can change the tag on an approved or reimbursed expense, and anyone can change the tag on an unapproved expense. + +**Can I set up multi-level tags in New Expensify?** + +At this time, only single-level tags are available in New Expensify. If you’ve used multi-level tags in Expensify Classic, you will see the first-level tag in New Expensify. Multi-level tags are under development. +{% include faq-end.md %} + +
diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md new file mode 100644 index 000000000000..294dcfc57a23 --- /dev/null +++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md @@ -0,0 +1,39 @@ +--- +title: Require tags and categories for expenses +description: Make tags and/or categories required for all expenses +--- +
+ +To require workspace members to add tags and/or categories to their expenses, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Scroll down and click **Workspaces** in the left menu. +3. Select a workspace. +4. Click **Tags** or **Categories** in the left menu. +5. Click **Settings** at the top right of the page. +6. Enable the “Members must tag/categorize all spend” toggle. +7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap **Workspaces**. +3. Select a workspace. +4. Tap **Tags** or **Categories**. +5. Tap **Settings** at the top right of the page. +6. Enable the “Members must tag/categorize all spend” toggle. +7. If desired, repeat steps 4-6 for tags or categories (whichever you haven’t done yet). +{% include end-option.html %} + +{% include end-selector.html %} + +This will highlight the tag and/or category field as required on all expenses. + +{% include info.html %} +Expenses will still be able to be submitted without a tag and/or category even if they are set as required. The submitter and approver will see an orange dot on the expense details alerting them that the tag/category is missing. +{% include end-info.html %} + +
diff --git a/docs/articles/new-expensify/workspaces/The-Free-Plan.md b/docs/articles/new-expensify/workspaces/The-Free-Plan.md deleted file mode 100644 index b036c5b087d2..000000000000 --- a/docs/articles/new-expensify/workspaces/The-Free-Plan.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: The Free Plan -description: Everything you need to know about Expensify's Free Plan! -redirect_from: articles/split-bills/workspaces/The-Free-Plan/ ---- - - - -# What is the Free Plan? -The free plan is ideal for start-ups and small businesses to manage expenses. With the Free Plan, a workspace admin can set their team up with Expensify Cards, reimburse cash expenses, send invoices, and manage bills, all for free! You will have total visibility and control over all spending associated with your workspace in real time. - -# Features Included with the Free Plan -- Expensify Cards for all employees -- Invoicing -- Bill Pay -- Unlimited receipt scanning for everyone in the company -- Free next-day ACH reimbursements for cash expenses -- Up to 4% [cash back](https://community.expensify.com/discussion/8454/4-cash-back-on-the-expensify-card-is-here-to-stay) -- Free corporate travel booking - -# Setting Up the Free Plan -- Navigate to new.expensify.com, enter your company email address, and set a password -- Click the **green “+”** button and select **_New workspace_** - -Once you’ve created your Workspace, you will receive a message from Concierge encouraging you to chat with your Setup Specialist. Click the link in the message, and your designated Setup Specialist will guide you through how to configure your company setup. - -Once you’ve completed your company setup, you should have completed the following tasks: - -- Connected a business bank account (Settings menu > Click **_Bank account_** and follow the prompts). -- Invited members to the workspace -- Assigned Expensify Cards - -# Inviting Members to the Free Plan -- Navigate to the Settings Menu and click **_Members_** to invite your team. You can invite employees one at a time, or you can invite multiple users by listing out their email addresses separated by a comma -- To use the Expensify Card, you must invite them to your workspace via your company email address (i.e., admin@companyemail.com and NOT admin@gmail.com). - -# Managing the Free Plan -## Workspace Settings - -To access your workspace settings, click your profile icon and then on your workspace name. This settings menu allows you to manage your workspace members, issue additional Expensify Cards, and utilize this plan’s various bill pay and payment options. - -## Paying an Expense Report -- Once a user creates an expense it will automatically be shared with you in a Processing report. -- Pay expenses directly through Expensify by choosing ‘Reimburse > via Direct Deposit (ACH)` in a report on www.expensify.com or by choosing ‘Pay with Expensify’ in a payment request on new.expensify.com. -- Notify your user that you’ll pay them manually outside of Expensify by choosing ‘Reimburse > I’ll do it manually’ in a report on www.expensify.com or choosing ‘Pay Elsewhere’ in a payment request on new.expensify.com. -- Reports with only non-reimbursable expenses on them have the option to ‘Mark as Closed’ in the report on www.expensify.com or ‘Mark as Done’ in the payment request on new.expensify.com. - -## Changing Submitted Expenses - -Request an edit an expense or remove an expense before you pay, you can let your user know by making a comment in the Report History section of their Processing report or chatting with them on new.expensify.com. - -# Managing Expenses - -## Creating an Expense -- You can create an expense either by swiping the Expensify card or just smartscan a receipt! -- Once you create an expense it will be automatically added to a report and shared with your admin. -- You can edit or delete any expense that hasn’t been paid or closed by your admin. - -## Getting paid for Expenses -- Automatic submission is already set up, so your admin can pay you back immediately once you create an expense. -- Your admin will get a notification when you send them a new expense, but you can also remind them to pay you by making a comment in the Report History section of your Processing report or chatting with them on new.expensify.com. - -{% include faq-begin.md %} - -## Do I need a business bank account to use the Free Plan? - -You will need a US business checking account if you want to enable the Expensify Card and set up direct ACH reimbursement. -You will need to take a few steps to verify your business bank account and connect it to Expensify. You will also need to set aside some time and have your ID ready. -If you're not in the US, you can still use the Free Plan, but the Expensify Card and direct reimbursement will not be available. - -## Can my workspace have more than one Admin? - -The Expensify Workplace only allows for one admin (the workspace creator). - -## I am on a paid plan, can I switch to a Free Plan? - -You can set up a Free Plan, but you must honor any active subscription you have also. If you're on a paid plan, it is likely you want more functionality than what the Free Plan offers such as a direct connection with an accounting integration and approval workflows. - -## Can I get cashback on Expensify Card purchases if I have a free plan? - -You can get 1% credited back to your settlement account once you spend over $25,000 per month across all cards and 2% when you spend over $250,000! - -## Can I switch the workspace currency on the Free Plan? It looks set to USD - -Yes, you can change the currency of the Free Plan by going to Avatar > [Workspace Name] > General settings > Default currency. - -We do require a US business bank account for reimbursements and Expensify Card settlements though, so if you have a business bank account linked to your account, then the currency of the Free Plan will be USD. - -## Is a free plan available to people outside the USA? - -Yes! You can use the free plan anywhere in the world to track expenses, send invoices etc. Just remember, anything that requires a verified business bank account (such as ACH reimbursement and the Expensify Card) is only available to those with a US checking account! - -## Can I integrate the free plan with my accounting package? - -No. In order to use our accounting sync, you will need to update to a paid plan - -## With the Free Plan, can I add my own categories? - -Categories are standardized on the Free Plan and can’t be edited. Custom categories and tags or accounting system integration are available on a paid plan. - -## With the Free Plan, can I export reports using a custom format? - -The Free Plan offers standard report export formats. You'll need to upgrade to a paid plan to create a custom export format. - -## Can I change the mileage rate or unit used for distance tracking? - -Yes. The workspace admin can access the rate and unit settings by going to Avatar > [Workspace name] > Reimburse expenses > Track distance. - -## Can I add non-Expensify cardholders to my Workspace? - -Yes. You can invite any user to your Workspace. Just click invite and enter their email and they will be invited to download the app and join! - -## Expenses are automatically shared with the admin on a Processing report on the Free Plan, can I change this? - -No. Expense reports submitted on the Free Plan are set to submit automatically and do not allow for approvals. Different Scheduled Submit settings are available on our paid plans. - -## How do I give a user I invited to my Workspace an Expensify Card? - -You can give a user an Expensify Card by going to the Company Cards Page on expensify.com and setting a card limit >$0. Please note that in order to give a user a card, they must be using a private company email address (i.e. name@companyname.com NOT name@gmail.com) - -## Can I use a different bank account to the one I have added for some of the features in the workspace? - -No. The bank account you have added will be used to issue corporate cards, reimburse expenses, collect invoices, and pay bills! - -## Can I upgrade my Free Workspace to a Paid Workspace or do I need to create a new one? - -There is no way to upgrade a workspace, so you would need to create a new Workspace. Paid workspaces have more functionalities than what the Free Plan offers, such as a direct connection with an accounting integration and approval workflows. - -## Can I create more than one Processing report at a time? - -No. To keep things simple, we only allow one Processing report per user at a time. If you need to have more than one report at a time, our paid plans support unlimited reports. - -## A user has added an expense I need to change before I pay it, how do I let them know? - -Users can edit and delete expenses on Processing reports. If you need something changed, let them know by commenting in the Report History section of the report on expensify.com or by chatting with them in new.expensify.com. - -## Can I ‘Reopen’ a report once it’s Reimbursed or Closed? - -No. Once an admin reimburses or closes a report, it cannot be ‘reopened’, but you can always comment on a report to add context. - -## I accidentally reimbursed a report too soon. Can I cancel the reimbursement? - -Depending on how quickly you report it to us, we may be able to help cancel a reimbursement. Chat Concierge to see if we can help cancel a reimbursement. - -## As an admin, can I edit users’ expenses and delete them from reports? - -No. Only users can edit and delete expenses on the Free plan. Admin control of submitted expenses on a workspace is a feature of our paid plans. If you need something changed, let the user know by commenting in the Report History section of the report on www.expensify.com or by chatting with them in new.expensify.com. - -{% include faq-end.md %} diff --git a/docs/redirects.csv b/docs/redirects.csv index f775d2f97094..c79c07ee3cf4 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -170,3 +170,5 @@ https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-a https://help.expensify.com/articles/expensify-classic/reports/The-Reports-Page,https://help.expensify.com/articles/expensify-classic/reports/Report-statuses https://help.expensify.com/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan,https://help.expensify.com/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan https://help.expensify.com/articles/new-expensify/bank-accounts-and-payments/Connect-a-Bank-Account,https://help.expensify.com/new-expensify/hubs/expenses/Connect-a-Bank-Account +https://help.expensify.com/articles/new-expensify/settings/Profile,https://help.expensify.com/new-expensify/hubs/settings/ +https://help.expensify.com/articles/new-expensify/expenses/Referral-Program.html,https://help.expensify.com/articles/expensify-classic/expensify-partner-program/Referral-Program diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4120e6accb86..532f014b0a72 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.72 + 1.4.73 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.72.1 + 1.4.73.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 11edf545cea4..b0b154900df0 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.72 + 1.4.73 CFBundleSignature ???? CFBundleVersion - 1.4.72.1 + 1.4.73.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1f777d1739e4..41dbf33a20b5 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.72 + 1.4.73 CFBundleVersion - 1.4.72.1 + 1.4.73.7 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index e2d9b6d2988b..b8eae21ecb09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "1.4.72-1", + "version": "1.4.73-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.72-1", + "version": "1.4.73-7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.69", + "@expensify/react-native-live-markdown": "0.1.70", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -78,7 +78,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.12", + "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -201,7 +201,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.2.0", + "electron": "^29.3.3", "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -248,8 +248,8 @@ "yaml": "^2.2.1" }, "engines": { - "node": "20.10.0", - "npm": "10.2.3" + "node": "20.13.0", + "npm": "10.5.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -3569,9 +3569,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.69.tgz", - "integrity": "sha512-ZJG6f06lHrNb0s/92JyyvsSDGGZLdU/a/YXir2A5UFCiERVWkgJxcugsYbEMemh2HsWD6GXvhq1Sngj2H620nw==", + "version": "0.1.70", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.70.tgz", + "integrity": "sha512-HyqBtZyvuJFB4gIUECKIMxWCnTPlPj+GPWmw80VzMBRFV9QiFRKUKRWefNEJ1cXV5hl8a6oOWDQla+dCnjCzOQ==", "engines": { "node": ">= 18.0.0" }, @@ -18568,9 +18568,9 @@ } }, "node_modules/electron": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-29.2.0.tgz", - "integrity": "sha512-ALKrCN52RG4g9prx4DriXSPnY5WoiyRUCNp7zEVQuoiNOpHTNqMMpRidQAHzntV4hajF1LMWHVoBkwqIs1jHhg==", + "version": "29.3.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.3.3.tgz", + "integrity": "sha512-I/USTe9UsQUKb/iuiYnmt074vHxNHCJZWYiU4Xg6lNPKVBsPadAhZcc+g2gYLqC1rA7KT4AvKTmNsY8n7oEUCw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -31059,16 +31059,16 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.12.tgz", - "integrity": "sha512-RSIYTwQVKWFqZKtmtzd4JU/FnsqdGPBtHu/N6xl7TsauAFnEouUJNjmC7Rg/pd010OX1UvyraQKdBIZ5Pf2q0A==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.13.tgz", + "integrity": "sha512-rF7NQZ26rJAI8ysRJaG71dl2c7AIq48ibbn7xCyF3lEZ/yOjA8BeR0utRwDjaHGtswQscgETboilhaaH5UtIYg==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" }, "engines": { - "node": "20.10.0", - "npm": "10.2.3" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "peerDependencies": { "lodash": "4.x", diff --git a/package.json b/package.json index c91a7a6acc4c..66f2018a2878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.72-1", + "version": "1.4.73-7", "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.", @@ -34,7 +34,7 @@ "android-build": "fastlane android build", "android-build-e2e": "bundle exec fastlane android build_e2e", "android-build-e2edelta": "bundle exec fastlane android build_e2edelta", - "test": "TZ=utc jest", + "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "typecheck": "tsc", "lint": "eslint . --max-warnings=0 --cache --cache-location=node_modules/.cache/eslint", "lint-changed": "eslint --fix $(git diff --diff-filter=AM --name-only main -- \"*.js\" \"*.ts\" \"*.tsx\")", @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.69", + "@expensify/react-native-live-markdown": "0.1.70", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -130,7 +130,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.12", + "react-fast-pdf": "1.0.13", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -253,7 +253,7 @@ "csv-parse": "^5.5.5", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^29.2.0", + "electron": "^29.3.3", "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -327,7 +327,7 @@ ] }, "engines": { - "node": "20.10.0", - "npm": "10.2.3" + "node": "20.13.0", + "npm": "10.5.2" } } diff --git a/patches/react-native+0.73.4+015+copyStateOnClone.patch b/patches/react-native+0.73.4+015+copyStateOnClone.patch new file mode 100644 index 000000000000..9c6bac903efb --- /dev/null +++ b/patches/react-native+0.73.4+015+copyStateOnClone.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp b/node_modules/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +index 641b6d2..6adeb1b 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/core/ShadowNode.cpp +@@ -331,6 +331,7 @@ ShadowNode::Unshared ShadowNode::cloneTree( + childNode = parentNode.clone({ + ShadowNodeFragment::propsPlaceholder(), + std::make_shared(children), ++ parentNode.getState(), + }); + } + diff --git a/src/CONST.ts b/src/CONST.ts index efb0463457b1..08fc5908d328 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -362,7 +362,6 @@ const CONST = { DEFAULT_ROOMS: 'defaultRooms', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', - TRACK_EXPENSE: 'trackExpense', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', SPOTNANA_TRAVEL: 'spotnanaTravel', @@ -556,8 +555,10 @@ const CONST = { EMPTY_ARRAY, EMPTY_OBJECT, USE_EXPENSIFY_URL, + STATUS_EXPENSIFY_URL: 'https://status.expensify.com', GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', + GOOGLE_CLOUD_URL: 'https://clients3.google.com/generate_204', IMAGE_BASE64_MATCH: 'base64', DEEPLINK_BASE_URL: 'new-expensify://', PDF_VIEWER_URL: '/pdf/web/viewer.html', @@ -937,9 +938,12 @@ const CONST = { MERCHANT: 'merchant', FROM: 'from', TO: 'to', + CATEGORY: 'category', + TAG: 'tag', TOTAL: 'total', TYPE: 'type', ACTION: 'action', + TAX_AMOUNT: 'taxAmount', }, PRIORITY_MODE: { GSD: 'gsd', @@ -1045,6 +1049,7 @@ const CONST = { MAX_RETRY_WAIT_TIME_MS: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, + BACKEND_CHECK_INTERVAL_MS: 60 * 1000, MAX_REQUEST_RETRIES: 10, NETWORK_STATUS: { ONLINE: 'online', @@ -1056,7 +1061,7 @@ const CONST = { DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, - DEFAULT_NETWORK_DATA: {isOffline: false}, + DEFAULT_NETWORK_DATA: {isOffline: false, isBackendReachable: true}, FORMS: { LOGIN_FORM: 'LoginForm', VALIDATE_CODE_FORM: 'ValidateCodeForm', @@ -1299,8 +1304,15 @@ const CONST = { XERO_CONFIG: { AUTO_SYNC: 'autoSync', SYNC: 'sync', + ENABLE_NEW_CATEGORIES: 'enableNewCategories', + EXPORT: 'export', IMPORT_CUSTOMERS: 'importCustomers', IMPORT_TAX_RATES: 'importTaxRates', + INVOICE_STATUS: { + AWAITING_PAYMENT: 'AWT_PAYMENT', + DRAFT: 'DRAFT', + AWAITING_APPROVAL: 'AWT_APPROVAL', + }, IMPORT_TRACKING_CATEGORIES: 'importTrackingCategories', MAPPINGS: 'mappings', TRACKING_CATEGORY_PREFIX: 'trackingCategory_', @@ -1320,6 +1332,12 @@ const CONST = { JOURNAL_ENTRY: 'journal_entry', }, + XERO_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + REPORT_EXPORTED: 'REPORT_EXPORTED', + REPORT_SUBMITTED: 'REPORT_SUBMITTED', + }, + QUICKBOOKS_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', @@ -1785,6 +1803,8 @@ const CONST = { XERO_SYNC_IMPORT_CUSTOMERS: 'xeroSyncImportCustomers', XERO_SYNC_IMPORT_BANK_ACCOUNTS: 'xeroSyncImportBankAccounts', XERO_SYNC_IMPORT_TAX_RATES: 'xeroSyncImportTaxRates', + XERO_CHECK_CONNECTION: 'xeroCheckConnection', + XERO_SYNC_TITLE: 'xeroSyncTitle', }, }, ACCESS_VARIANTS: { @@ -1954,7 +1974,7 @@ const CONST = { POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/, - SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*`, 'gim'), + SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#@]+(?:\\.[\\w\\-\\'\\+]+)*(?![^\`]*\`)`, 'gim'), }, PRONOUNS: { @@ -4746,13 +4766,24 @@ const CONST = { CARD: 'card', DISTANCE: 'distance', }, + + SEARCH_BOTTOM_TAB_URL: '/Search_Bottom_Tab', + + SEARCH_DATA_TYPES: { + TRANSACTION: 'transaction', + }, + + REFERRER: { + NOTIFICATION: 'notification', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; type IOUType = ValueOf; type IOUAction = ValueOf; +type IOURequestType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType}; export default CONST; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 0cf377a4bd3b..6205afb9c03c 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -142,8 +142,10 @@ function Expensify({ // Initialize this client as being an active client ActiveClientManager.init(); - // Used for the offline indicator appearing when someone is offline - NetworkConnection.subscribeToNetInfo(); + // Used for the offline indicator appearing when someone is offline or backend is unreachable + const unsubscribeNetworkStatus = NetworkConnection.subscribeToNetworkStatus(); + + return () => unsubscribeNetworkStatus(); }, []); useEffect(() => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 804c8dadd553..0405edbd4189 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -336,6 +336,7 @@ const ONYXKEYS = { WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', + REPORT_NAME_VALUE_PAIRS: 'reportNameValuePairs_', REPORT_DRAFT: 'reportDraft_', // REPORT_METADATA is a perf optimization used to hold loading states (isLoadingInitialReportActions, isLoadingOlderReportActions, isLoadingNewerReportActions). // A lot of components are connected to the Report entity and do not care about the actions. Setting the loading state @@ -548,6 +549,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs; [ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string; [ONYXKEYS.COLLECTION.REPORT]: OnyxTypes.Report; + [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: OnyxTypes.ReportNameValuePairs; [ONYXKEYS.COLLECTION.REPORT_DRAFT]: OnyxTypes.Report; [ONYXKEYS.COLLECTION.REPORT_METADATA]: OnyxTypes.ReportMetadata; [ONYXKEYS.COLLECTION.REPORT_ACTIONS]: OnyxTypes.ReportActions; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index afe806c193aa..0dff2992fe91 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -125,7 +125,10 @@ const ROUTES = { SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 SETTINGS_ENABLE_PAYMENTS_REFACTOR: 'settings/wallet/enable-payments-refactor', + // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 + SETTINGS_ENABLE_PAYMENTS_TEMPORARY_TERMS: 'settings/wallet/enable-payments-temporary-terms', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, @@ -209,7 +212,11 @@ const ROUTES = { REPORT: 'r', REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', - getRoute: (reportID: string, reportActionID?: string) => (reportActionID ? (`r/${reportID}/${reportActionID}` as const) : (`r/${reportID}` as const)), + getRoute: (reportID: string, reportActionID?: string, referrer?: string) => { + const baseRoute = reportActionID ? (`r/${reportID}/${reportActionID}` as const) : (`r/${reportID}` as const); + const referrerParam = referrer ? `?referrer=${encodeURIComponent(referrer)}` : ''; + return `${baseRoute}${referrerParam}` as const; + }, }, REPORT_AVATAR: { route: 'r/:reportID/avatar', @@ -665,8 +672,8 @@ const ROUTES = { getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/settings` as const, }, WORKSPACE_EDIT_TAGS: { - route: 'settings/workspaces/:policyID/tags/edit', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/edit` as const, + route: 'settings/workspaces/:policyID/tags/:orderWeight/edit', + getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tags/${orderWeight}/edit` as const, }, WORKSPACE_TAG_EDIT: { route: 'settings/workspace/:policyID/tag/:tagName/edit', @@ -676,6 +683,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag/:tagName', getRoute: (policyID: string, tagName: string) => `settings/workspaces/${policyID}/tag/${encodeURIComponent(tagName)}` as const, }, + WORKSPACE_TAG_LIST_VIEW: { + route: 'settings/workspaces/:policyID/tag-list/:orderWeight', + getRoute: (policyID: string, orderWeight: number) => `settings/workspaces/${policyID}/tag-list/${orderWeight}` as const, + }, WORKSPACE_TAXES: { route: 'settings/workspaces/:policyID/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const, @@ -776,6 +787,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import` as const, }, + POLICY_ACCOUNTING_XERO_CHART_OF_ACCOUNTS: { + route: 'settings/workspaces/:policyID/accounting/xero/import/accounts', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/import/accounts` as const, + }, POLICY_ACCOUNTING_XERO_ORGANIZATION: { route: 'settings/workspaces/:policyID/accounting/xero/organization/:currentOrganizationID', getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` as const, @@ -804,6 +819,18 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/export', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export` as const, }, + POLICY_ACCOUNTING_XERO_PREFERRED_EXPORTER_SELECT: { + route: '/settings/workspaces/:policyID/connections/xero/export/preferred-exporter/select', + getRoute: (policyID: string) => `/settings/workspaces/${policyID}/connections/xero/export/preferred-exporter/select` as const, + }, + POLICY_ACCOUNTING_XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: { + route: 'settings/workspaces/:policyID/accounting/xero/export/purchase-bill-date-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/purchase-bill-date-select` as const, + }, + POLICY_ACCOUNTING_XERO_EXPORT_BANK_ACCOUNT_SELECT: { + route: 'settings/workspaces/:policyID/accounting/xero/export/bank-account-select', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/export/bank-account-select` as const, + }, POLICY_ACCOUNTING_XERO_ADVANCED: { route: 'settings/workspaces/:policyID/accounting/xero/advanced', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const, @@ -812,6 +839,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const, }, + POLICY_ACCOUNTING_XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: { + route: 'settings/workspaces/:policyID/accounting/xero/advanced/bill-payment-account-selector', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/bill-payment-account-selector` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b76df331fe74..e25b9776919a 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,7 +27,6 @@ const SCREENS = { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', }, - WORKSPACES_CENTRAL_PANE: 'WorkspacesCentralPane', SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', REPORT_RHP: 'Search_Report_RHP', @@ -90,7 +89,10 @@ const SCREENS = { TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 ENABLE_PAYMENTS_REFACTOR: 'Settings_Wallet_EnablePayments_Refactor', + // TODO: Added temporarily for testing purposes, remove after refactor - https://github.com/Expensify/App/issues/36648 + ENABLE_PAYMENTS_TEMPORARY_TERMS: 'Settings_Wallet_EnablePayments_Temporary_Terms', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', @@ -109,9 +111,6 @@ const SCREENS = { CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, - WORKSPACE_SWITCHER: { - ROOT: 'WorkspaceSwitcher_Root', - }, RIGHT_MODAL: { SETTINGS: 'Settings', NEW_CHAT: 'NewChat', @@ -242,14 +241,19 @@ const SCREENS = { QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector', XERO_IMPORT: 'Policy_Accounting_Xero_Import', XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers', + XERO_CHART_OF_ACCOUNTS: 'Policy_Accounting_Xero_Import_Chart_Of_Accounts', XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer', XERO_TAXES: 'Policy_Accounting_Xero_Taxes', XERO_TRACKING_CATEGORIES: 'Policy_Accounting_Xero_Tracking_Categories', XERO_MAP_COST_CENTERS: 'Policy_Accounting_Xero_Map_Cost_Centers', XERO_MAP_REGION: 'Policy_Accounting_Xero_Map_Region', XERO_EXPORT: 'Policy_Accounting_Xero_Export', + XERO_EXPORT_PURCHASE_BILL_DATE_SELECT: 'Policy_Accounting_Xero_Export_Purchase_Bill_Date_Select', XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced', XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector', + XERO_EXPORT_PREFERRED_EXPORTER_SELECT: 'Workspace_Accounting_Xero_Export_Preferred_Exporter_Select', + XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', + XERO_EXPORT_BANK_ACCOUNT_SELECT: 'Policy_Accounting_Xero_Export_Bank_Account_Select', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -280,6 +284,7 @@ const SCREENS = { TAX_CREATE: 'Workspace_Tax_Create', TAG_CREATE: 'Tag_Create', TAG_SETTINGS: 'Tag_Settings', + TAG_LIST_VIEW: 'Tag_List_View', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', WORKFLOWS: 'Workspace_Workflows', @@ -355,7 +360,6 @@ const SCREENS = { }, ROOM_MEMBERS_ROOT: 'RoomMembers_Root', ROOM_INVITE_ROOT: 'RoomInvite_Root', - CHAT_FINDER_ROOT: 'ChatFinder_Root', FLAG_COMMENT_ROOT: 'FlagComment_Root', REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', GET_ASSISTANCE: 'GetAssistance', diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx new file mode 100644 index 000000000000..b977903d3adc --- /dev/null +++ b/src/components/AccountingListSkeletonView.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import {Circle, Rect} from 'react-native-svg'; +import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; + +type AccountingListSkeletonViewProps = { + shouldAnimate?: boolean; +}; + +function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { + return ( + ( + <> + + + + )} + /> + ); +} + +AccountingListSkeletonView.displayName = 'AccountingListSkeletonView'; + +export default AccountingListSkeletonView; diff --git a/src/components/AmountPicker/index.tsx b/src/components/AmountPicker/index.tsx index 45e511f24748..014932f7736b 100644 --- a/src/components/AmountPicker/index.tsx +++ b/src/components/AmountPicker/index.tsx @@ -1,17 +1,15 @@ import React, {forwardRef, useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; -import FormHelpMessage from '@components/FormHelpMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import callOrReturn from '@src/types/utils/callOrReturn'; import AmountSelectorModal from './AmountSelectorModal'; import type {AmountPickerProps} from './types'; function AmountPicker({value, description, title, errorText = '', onInputChange, furtherDetails, rightLabel, ...rest}: AmountPickerProps, forwardedRef: ForwardedRef) { - const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -43,11 +41,10 @@ function AmountPicker({value, description, title, errorText = '', onInputChange, description={description} onPress={showPickerModal} furtherDetails={furtherDetails} + brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} rightLabel={rightLabel} + errorText={errorText} /> - - - ; - - /** The report associated with the receipt attachment, if any */ - parentReport: OnyxEntry; - - /** The policy associated with the receipt attachment, if any */ - policy: OnyxEntry; - - /** The list of report actions associated with the receipt attachment, if any */ - parentReportActions: OnyxEntry; }; type ImagePickerResponse = { @@ -157,10 +148,7 @@ function AttachmentModal({ isWorkspaceAvatar = false, maybeIcon = false, transaction, - parentReport, - parentReportActions, headerTitle, - policy, children, fallbackSource, canEditReceipt = false, @@ -413,7 +401,7 @@ function AttachmentModal({ const sourceForAttachmentView = sourceState || source; const threeDotsMenuItems = useMemo(() => { - if (!isReceiptAttachment || !parentReport || !parentReportActions) { + if (!isReceiptAttachment) { return []; } @@ -454,7 +442,7 @@ function AttachmentModal({ } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isReceiptAttachment, parentReport, parentReportActions, policy, transaction, file, sourceState, iouType]); + }, [isReceiptAttachment, transaction, file, sourceState, iouType]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. @@ -468,7 +456,7 @@ function AttachmentModal({ } const context = useMemo( () => ({ - pagerItems: [], + pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}], activePage: 0, pagerRef: undefined, isPagerScrolling: nope, @@ -477,7 +465,7 @@ function AttachmentModal({ onScaleChanged: () => {}, onSwipeDown: closeModal, }), - [closeModal, nope], + [closeModal, nope, sourceForAttachmentView], ); return ( @@ -621,16 +609,6 @@ export default withOnyx({ }, initWithStoredValues: false, }, - parentReport: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report ? report.parentReportID : '0'}`, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : '0'}`, - canEvict: false, - }, })(memo(AttachmentModal)); export type {Attachment, FileObject, ImagePickerResponse}; diff --git a/src/components/AttachmentOfflineIndicator.tsx b/src/components/AttachmentOfflineIndicator.tsx new file mode 100644 index 000000000000..d425e6f18e0e --- /dev/null +++ b/src/components/AttachmentOfflineIndicator.tsx @@ -0,0 +1,57 @@ +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +type AttachmentOfflineIndicatorProps = { + /** Whether the offline indicator is displayed for the attachment preview. */ + isPreview?: boolean; +}; + +function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndicatorProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + + // We don't want to show the offline indicator when the attachment is a cached one, so + // we delay the display by 200 ms to ensure it is not a cached one. + const [onCacheDelay, setOnCacheDelay] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => setOnCacheDelay(false), 200); + + return () => clearTimeout(timeout); + }, []); + + if (!isOffline || onCacheDelay) { + return null; + } + + return ( + + + {!isPreview && ( + + {translate('common.youAppearToBeOffline')} + {translate('common.attachementWillBeAvailableOnceBackOnline')} + + )} + + ); +} + +AttachmentOfflineIndicator.displayName = 'AttachmentOfflineIndicator'; + +export default AttachmentOfflineIndicator; diff --git a/src/components/CollapsibleSection/index.tsx b/src/components/CollapsibleSection/index.tsx index f984906595c2..d339f005e3d3 100644 --- a/src/components/CollapsibleSection/index.tsx +++ b/src/components/CollapsibleSection/index.tsx @@ -18,6 +18,9 @@ type CollapsibleSectionProps = ChildrenProps & { /** Style of title of the collapsible section */ titleStyle?: StyleProp; + /** Style for the text */ + textStyle?: StyleProp; + /** Style for the wrapper view */ wrapperStyle?: StyleProp; @@ -25,7 +28,7 @@ type CollapsibleSectionProps = ChildrenProps & { shouldShowSectionBorder?: boolean; }; -function CollapsibleSection({title, children, titleStyle, wrapperStyle, shouldShowSectionBorder}: CollapsibleSectionProps) { +function CollapsibleSection({title, children, titleStyle, textStyle, wrapperStyle, shouldShowSectionBorder}: CollapsibleSectionProps) { const theme = useTheme(); const styles = useThemeStyles(); const [isExpanded, setIsExpanded] = useState(false); @@ -50,7 +53,7 @@ function CollapsibleSection({title, children, titleStyle, wrapperStyle, shouldSh pressDimmingValue={0.2} > {title} diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index 3a5e545cce88..7e1d81cc4071 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -9,6 +9,8 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; import {getQuickBooksOnlineSetupLink} from '@libs/actions/connections/QuickBooksOnline'; import CONST from '@src/CONST'; @@ -29,9 +31,11 @@ function ConnectToQuickbooksOnlineButton({ shouldDisconnectIntegrationBeforeConnecting, integrationToDisconnect, }: ConnectToQuickbooksOnlineButtonProps & ConnectToQuickbooksOnlineButtonOnyxProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); const webViewRef = useRef(null); const [isWebViewOpen, setWebViewOpen] = useState(false); + const {isOffline} = useNetwork(); const authToken = session?.authToken ?? null; @@ -48,7 +52,9 @@ function ConnectToQuickbooksOnlineButton({ setWebViewOpen(true); }} text={translate('workspace.accounting.setup')} + style={styles.justifyContentCenter} small + isDisabled={isOffline} /> {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( ; const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); @@ -46,6 +49,7 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo text={translate('workspace.accounting.setup')} style={styles.justifyContentCenter} small + isDisabled={isOffline} /> {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( | undefined; - /** Style of the subtitle text */ - subTitleStyle?: StyleProp | undefined; - /** Whether to use ScrollView or not */ shouldUseScrollView?: boolean; }; -type ConnectionLayoutContentProps = Pick; +type ConnectionLayoutContentProps = Pick; -function ConnectionLayoutContent({title, titleStyle, subtitle, subTitleStyle, children}: ConnectionLayoutContentProps) { +function ConnectionLayoutContent({title, titleStyle, children}: ConnectionLayoutContentProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); return ( <> {title && {translate(title)}} - {subtitle && {translate(subtitle)}} {children} ); @@ -70,13 +66,12 @@ function ConnectionLayout({ headerTitle, children, title, - subtitle, + headerSubtitle, policyID, accessVariants, featureName, contentContainerStyle, titleStyle, - subTitleStyle, shouldUseScrollView = true, }: ConnectionLayoutProps) { const {translate} = useLocalize(); @@ -85,14 +80,12 @@ function ConnectionLayout({ () => ( {children} ), - [title, subtitle, titleStyle, subTitleStyle, children], + [title, titleStyle, children], ); return ( @@ -108,6 +101,7 @@ function ConnectionLayout({ > Navigation.goBack()} /> {shouldUseScrollView ? ( diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index dc0201747da2..002c0c6d4b0a 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -1,14 +1,14 @@ import {useIsFocused} from '@react-navigation/native'; import React, {forwardRef, useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; -import {View} from 'react-native'; +import type {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {MaybePhraseKey} from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import FormHelpMessage from './FormHelpMessage'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; type CountrySelectorProps = { @@ -53,23 +53,20 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = () }, [countryCode]); return ( - - { - const activeRoute = Navigation.getActiveRouteWithoutParams(); - didOpenContrySelector.current = true; - Navigation.navigate(ROUTES.SETTINGS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute)); - }} - /> - - - - + { + const activeRoute = Navigation.getActiveRouteWithoutParams(); + didOpenContrySelector.current = true; + Navigation.navigate(ROUTES.SETTINGS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute)); + }} + /> ); } diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx index fde188fc70f2..065099867e14 100644 --- a/src/components/DecisionModal.tsx +++ b/src/components/DecisionModal.tsx @@ -1,17 +1,11 @@ import React from 'react'; import {View} from 'react-native'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import Button from './Button'; import Header from './Header'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; import Modal from './Modal'; -import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; -import Tooltip from './Tooltip'; type DecisionModalProps = { /** Title describing purpose of modal */ @@ -43,8 +37,6 @@ type DecisionModalProps = { }; function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, onFirstOptionSubmit, onSecondOptionSubmit, isSmallScreenWidth, onClose, isVisible}: DecisionModalProps) { - const {translate} = useLocalize(); - const theme = useTheme(); const styles = useThemeStyles(); return ( @@ -60,21 +52,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o title={title} containerStyles={[styles.alignItemsCenter]} /> - - - - - - {prompt}
{firstOptionText && ( diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 2f09b27f3067..c9d60f3ced46 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -2,7 +2,6 @@ import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import useKeyboardState from '@hooks/useKeyboardState'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,7 +14,6 @@ type FixedFooterProps = { }; function FixedFooter({style, children}: FixedFooterProps) { - const {isKeyboardShown} = useKeyboardState(); const insets = useSafeAreaInsets(); const styles = useThemeStyles(); @@ -23,7 +21,7 @@ function FixedFooter({style, children}: FixedFooterProps) { return null; } - const shouldAddBottomPadding = isKeyboardShown || !insets.bottom; + const shouldAddBottomPadding = !insets.bottom; return {children}; } diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 1ba633e5c9fe..5c74fd466a15 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -118,6 +118,7 @@ function FormWrapper({ enabledWhenOffline={enabledWhenOffline} isSubmitActionDangerous={isSubmitActionDangerous} disablePressOnEnter={disablePressOnEnter} + enterKeyEventListenerPriority={1} /> )} diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index a4c8842b3113..137012478549 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -54,6 +54,9 @@ type FormAlertWithSubmitButtonProps = { /** Style for the error message for submit button */ errorMessageStyle?: StyleProp; + + /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ + enterKeyEventListenerPriority?: number; }; function FormAlertWithSubmitButton({ @@ -73,6 +76,7 @@ function FormAlertWithSubmitButton({ onSubmit, useSmallerSubmitButtonSize = false, errorMessageStyle, + enterKeyEventListenerPriority = 0, }: FormAlertWithSubmitButtonProps) { const styles = useThemeStyles(); const style = [!footerContent ? {} : styles.mb3, buttonStyles]; @@ -102,6 +106,7 @@ function FormAlertWithSubmitButton({