diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4f78ee6b69bf..c106a25c8ab1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,5 @@ # Every PR gets a review from an internal Expensify engineer * @Expensify/pullerbear + +# Every PR that touches redirects gets reviewed by ring0 +docs/redirects.csv @Expensify/infra \ No newline at end of file diff --git a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js index 61e0eac5dc28..ad1668ecd597 100644 --- a/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js +++ b/.github/actions/javascript/getDeployPullRequestList/getDeployPullRequestList.js @@ -5,58 +5,42 @@ const ActionUtils = require('../../../libs/ActionUtils'); const GitUtils = require('../../../libs/GitUtils'); const GithubUtils = require('../../../libs/GithubUtils'); -const inputTag = core.getInput('TAG', {required: true}); - -const isProductionDeploy = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); -const itemToFetch = isProductionDeploy ? 'release' : 'tag'; +async function run() { + try { + const inputTag = core.getInput('TAG', {required: true}); + const isProductionDeploy = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); + const deployEnv = isProductionDeploy ? 'production' : 'staging'; + + console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`); + + const completedDeploys = ( + await GithubUtils.octokit.actions.listWorkflowRuns({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + workflow_id: 'platformDeploy.yml', + status: 'completed', + event: isProductionDeploy ? 'release' : 'push', + }) + ).data.workflow_runs; + + const inputTagIndex = _.findIndex(completedDeploys, (workflowRun) => workflowRun.head_branch === inputTag); + if (inputTagIndex < 0) { + throw new Error(`No completed deploy found for input tag ${inputTag}`); + } -/** - * Gets either releases or tags for a GitHub repo - * - * @param {boolean} fetchReleases - * @returns {*} - */ -function getTagsOrReleases(fetchReleases) { - if (fetchReleases) { - return GithubUtils.octokit.repos.listReleases({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - }); + const priorTag = completedDeploys[inputTagIndex + 1].head_branch; + console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`); + const prList = await GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); + console.log(`Found the pull request list: ${prList}`); + core.setOutput('PR_LIST', prList); + } catch (err) { + console.error(err.message); + core.setFailed(err); } - - return GithubUtils.octokit.repos.listTags({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - }); } -console.log(`Fetching ${itemToFetch} list from github...`); -getTagsOrReleases(isProductionDeploy) - .catch((githubError) => core.setFailed(githubError)) - .then(({data}) => { - const keyToPluck = isProductionDeploy ? 'tag_name' : 'name'; - const tags = _.pluck(data, keyToPluck); - const priorTagIndex = _.indexOf(tags, inputTag) + 1; - - if (priorTagIndex === 0) { - console.log(`No ${itemToFetch} was found for input tag ${inputTag}. Comparing it to latest ${itemToFetch} ${tags[0]}`); - } - - if (priorTagIndex === tags.length) { - const err = new Error("Somehow, the input tag was at the end of the paginated result, so we don't have the prior tag"); - console.error(err.message); - core.setFailed(err); - return; - } - - const priorTag = tags[priorTagIndex]; - console.log(`Given ${itemToFetch}: ${inputTag}`); - console.log(`Prior ${itemToFetch}: ${priorTag}`); +if (require.main === module) { + run(); +} - return GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); - }) - .then((pullRequestList) => { - console.log(`Found the pull request list: ${pullRequestList}`); - return core.setOutput('PR_LIST', pullRequestList); - }) - .catch((error) => core.setFailed(error)); +module.exports = run; diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 96a6b0c4c2ac..a795b2bab9a7 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -4,6 +4,59 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ +/***/ 5847: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const _ = __nccwpck_require__(5067); +const core = __nccwpck_require__(2186); +const github = __nccwpck_require__(5438); +const ActionUtils = __nccwpck_require__(970); +const GitUtils = __nccwpck_require__(669); +const GithubUtils = __nccwpck_require__(7999); + +async function run() { + try { + const inputTag = core.getInput('TAG', {required: true}); + const isProductionDeploy = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); + const deployEnv = isProductionDeploy ? 'production' : 'staging'; + + console.log(`Looking for PRs deployed to ${deployEnv} in ${inputTag}...`); + + const completedDeploys = ( + await GithubUtils.octokit.actions.listWorkflowRuns({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + workflow_id: 'platformDeploy.yml', + status: 'completed', + event: isProductionDeploy ? 'release' : 'push', + }) + ).data.workflow_runs; + + const inputTagIndex = _.findIndex(completedDeploys, (workflowRun) => workflowRun.head_branch === inputTag); + if (inputTagIndex < 0) { + throw new Error(`No completed deploy found for input tag ${inputTag}`); + } + + const priorTag = completedDeploys[inputTagIndex + 1].head_branch; + console.log(`Looking for PRs deployed to ${deployEnv} between ${priorTag} and ${inputTag}`); + const prList = await GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); + console.log(`Found the pull request list: ${prList}`); + core.setOutput('PR_LIST', prList); + } catch (err) { + console.error(err.message); + core.setFailed(err); + } +} + +if (require.main === require.cache[eval('__filename')]) { + run(); +} + +module.exports = run; + + +/***/ }), + /***/ 970: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { @@ -19556,74 +19609,12 @@ module.exports = JSON.parse('[[[0,44],"disallowed_STD3_valid"],[[45,46],"valid"] /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; /******/ /************************************************************************/ -var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. -(() => { -const _ = __nccwpck_require__(5067); -const core = __nccwpck_require__(2186); -const github = __nccwpck_require__(5438); -const ActionUtils = __nccwpck_require__(970); -const GitUtils = __nccwpck_require__(669); -const GithubUtils = __nccwpck_require__(7999); - -const inputTag = core.getInput('TAG', {required: true}); - -const isProductionDeploy = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', {required: false}, false); -const itemToFetch = isProductionDeploy ? 'release' : 'tag'; - -/** - * Gets either releases or tags for a GitHub repo - * - * @param {boolean} fetchReleases - * @returns {*} - */ -function getTagsOrReleases(fetchReleases) { - if (fetchReleases) { - return GithubUtils.octokit.repos.listReleases({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - }); - } - - return GithubUtils.octokit.repos.listTags({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - }); -} - -console.log(`Fetching ${itemToFetch} list from github...`); -getTagsOrReleases(isProductionDeploy) - .catch((githubError) => core.setFailed(githubError)) - .then(({data}) => { - const keyToPluck = isProductionDeploy ? 'tag_name' : 'name'; - const tags = _.pluck(data, keyToPluck); - const priorTagIndex = _.indexOf(tags, inputTag) + 1; - - if (priorTagIndex === 0) { - console.log(`No ${itemToFetch} was found for input tag ${inputTag}. Comparing it to latest ${itemToFetch} ${tags[0]}`); - } - - if (priorTagIndex === tags.length) { - const err = new Error("Somehow, the input tag was at the end of the paginated result, so we don't have the prior tag"); - console.error(err.message); - core.setFailed(err); - return; - } - - const priorTag = tags[priorTagIndex]; - console.log(`Given ${itemToFetch}: ${inputTag}`); - console.log(`Prior ${itemToFetch}: ${priorTag}`); - - return GitUtils.getPullRequestsMergedBetween(priorTag, inputTag); - }) - .then((pullRequestList) => { - console.log(`Found the pull request list: ${pullRequestList}`); - return core.setOutput('PR_LIST', pullRequestList); - }) - .catch((error) => core.setFailed(error)); - -})(); - -module.exports = __webpack_exports__; +/******/ +/******/ // startup +/******/ // Load entry module and return exports +/******/ // This entry module is referenced by other modules so it can't be inlined +/******/ var __webpack_exports__ = __nccwpck_require__(5847); +/******/ module.exports = __webpack_exports__; +/******/ /******/ })() ; diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh new file mode 100755 index 000000000000..04f55e19b4fb --- /dev/null +++ b/.github/scripts/createHelpRedirects.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# +# Adds new routes to the Cloudflare Bulk Redirects list for communityDot to helpDot +# pages. Does some basic sanity checking. + +set -e + +source scripts/shellUtils.sh + +info "Adding any new redirects from communityDot to helpDot" + +declare -r LIST_ID="20eb13215038446a98fd69ccf6d1026d" +declare -r ZONE_ID="$CLOUDFLARE_ACCOUNT_ID" +declare -r REDIRECTS_FILE="docs/redirects.csv" + +function checkCloudflareResult { + RESULTS=$1 + RESULT_MESSAGE=$(echo "$RESULTS" | jq .success) + + if ! [[ "$RESULT_MESSAGE" == "true" ]]; then + ERROR_MESSAGE=$(echo "$RESULTS" | jq .errors) + error "Error calling Cloudfalre API: $ERROR_MESSAGE" + exit 1 + fi +} + +declare -a ITEMS_TO_ADD + +while read -r line; do + # Split each line of the file into a source and destination so we can sanity check + # and compare against the current list. + read -r -a LINE_PARTS < <(echo "$line" | tr ',' ' ') + SOURCE_URL=${LINE_PARTS[0]} + DEST_URL=${LINE_PARTS[1]} + + # Make sure the format of the line is as execpted. + if [[ "${#LINE_PARTS[@]}" -gt 2 ]]; then + error "Found a line with more than one comma: $line" + exit 1 + fi + + # Basic sanity checking to make sure that the source and destination are in expected + # subdomains. + if ! [[ $SOURCE_URL =~ ^https://community\.expensify\.com ]]; then + error "Found source URL that is not a community URL: $SOURCE_URL" + exit 1 + fi + + if ! [[ $DEST_URL =~ ^https://help\.expensify\.com ]]; then + error "Found destination URL that is not a help URL: $DEST_URL" + exit 1 + fi + + info "Source: $SOURCE_URL and destination: $DEST_URL appear to be formatted correctly." + + ITEMS_TO_ADD+=("$line") + +# This line skips the first line in the csv because the first line is a header row. +done <<< "$(tail +2 $REDIRECTS_FILE)" + +# Sanity check that we should actually be running this and we aren't about to delete +# every single redirect. +if [[ "${#ITEMS_TO_ADD[@]}" -lt 1 ]]; then + error "No items found to add, why are we running?" + exit 1 +fi + +# This block builds a single JSON object with all of our updates so we can +# reduce the number of API calls we make. You cannot add any logging or anything +# that prints to std out to this block or it will break. We capture all of the std out +# from this loop and pass it to jq to build the json object. Any non-json will break the +# jq call at the end. +PUT_JSON=$(for new in "${ITEMS_TO_ADD[@]}"; do + read -r -a LINE_PARTS < <(echo "$new" | tr ',' ' ') + SOURCE_URL=${LINE_PARTS[0]} + DEST_URL=${LINE_PARTS[1]} + + # We strip the prefix here so that the rule will match both http and https. Since vanilla will eventially be removed, + # we need to catch both because we will not have the http > https redirect done by vanilla anymore. + NO_PREFIX_SOURCE_URL=${SOURCE_URL/https:\/\//} + jq -n --arg source "$NO_PREFIX_SOURCE_URL" --arg dest "$DEST_URL" '{"redirect": {source_url: $source, target_url: $dest}}' +done | jq -n '. |= [inputs]') + +info "Adding redirects for $PUT_JSON" + +# We use PUT here instead of POST so that we replace the entire list in place. This has many benefits: +# 1. We don't have to check if items are already in the list, allowing this script to run faster +# 2. We can support deleting redirects this way by simply removing them from the list +# 3. We can support updating redirects this way, in the case of typos or moved destinations. +# +# Additionally this API call is async, so after we finish it, we must poll to wait for it to finish to +# to know that it was actually completed. +PUT_RESULT=$(curl -s --request PUT --url "https://api.cloudflare.com/client/v4/accounts/$ZONE_ID/rules/lists/$LIST_ID/items" \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer $CLOUDFLARE_LIST_TOKEN" \ + --data "$PUT_JSON") + +checkCloudflareResult "$PUT_RESULT" +OPERATION_ID=$(echo "$PUT_RESULT" | jq -r .result.operation_id) + +DONE=false + +# Poll for completition +while [[ $DONE == false ]]; do + CHECK_RESULT=$(curl -s --request GET --url "https://api.cloudflare.com/client/v4/accounts/$ZONE_ID/rules/lists/bulk_operations/$OPERATION_ID" \ + --header 'Content-Type: application/json' \ + --header "Authorization: Bearer $CLOUDFLARE_LIST_TOKEN") + checkCloudflareResult "$CHECK_RESULT" + + STATUS=$(echo "$CHECK_RESULT" | jq -r .result.status) + + # Exit on completed or failed, other options are pending or running, in both cases + # we want to keep polling. + if [[ $STATUS == "completed" ]]; then + DONE=true + fi + + if [[ $STATUS == "failed" ]]; then + ERROR_MESSAGE=$(echo "$CHECK_RESULT" | jq .result.error) + error "List update failed with error: $ERROR_MESSAGE" + exit 1 + fi +done + +success "Updated lists successfully" diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index f30d55c4dff4..d494ea0d008b 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -12,6 +12,10 @@ env: SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }} DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: true + jobs: validateActor: runs-on: ubuntu-latest @@ -420,7 +424,7 @@ jobs: postGithubComment: name: Post a GitHub comment when platforms are done building and deploying runs-on: ubuntu-latest - if: ${{ always() }} + if: ${{ !cancelled() }} needs: [android, desktop, iOS, web] steps: - name: Checkout diff --git a/.github/workflows/updateHelpDotRedirects.yml b/.github/workflows/updateHelpDotRedirects.yml new file mode 100644 index 000000000000..531b8a3812fd --- /dev/null +++ b/.github/workflows/updateHelpDotRedirects.yml @@ -0,0 +1,31 @@ +name: Update Redirects to ExpensifyHelp + +on: + # Run on any push to main that has changes to the redirects file + push: + branches: + - main + paths: + - 'docs/redirects.csv' + + # Run on any manual trigger + workflow_dispatch: + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "redirects" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + + - name: Create help dot redirect + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_LIST_TOKEN: ${{ secrets.CLOUDFLARE_LIST_TOKEN }} + run: ./.github/scripts/createHelpRedirects.sh diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index fe58f3c0a6d8..6717c1736f65 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -24,8 +24,7 @@ const custom = require('../config/webpack/webpack.common')({ module.exports = ({config}) => { config.resolve.alias = { 'react-native-config': 'react-web-config', - 'react-native$': '@expensify/react-native-web', - 'react-native-web': '@expensify/react-native-web', + 'react-native$': 'react-native-web', '@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.js'), '@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'), diff --git a/android/app/build.gradle b/android/app/build.gradle index c7974572e665..6dc1f7b011a4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001039600 - versionName "1.3.96-0" + versionCode 1001039701 + versionName "1.3.97-1" } flavorDimensions "default" diff --git a/assets/emojis/common.js b/assets/emojis/common.ts similarity index 99% rename from assets/emojis/common.js rename to assets/emojis/common.ts index b7593b6e2960..cbefb21cf2d6 100644 --- a/assets/emojis/common.js +++ b/assets/emojis/common.ts @@ -1,12 +1,13 @@ -import Smiley from '../images/emoji.svg'; -import Flags from '../images/emojiCategoryIcons/flag.svg'; -import FoodAndDrink from '../images/emojiCategoryIcons/hamburger.svg'; -import Objects from '../images/emojiCategoryIcons/light-bulb.svg'; -import Symbols from '../images/emojiCategoryIcons/peace-sign.svg'; -import TravelAndPlaces from '../images/emojiCategoryIcons/plane.svg'; -import AnimalsAndNature from '../images/emojiCategoryIcons/plant.svg'; -import Activities from '../images/emojiCategoryIcons/soccer-ball.svg'; -import FrequentlyUsed from '../images/history.svg'; +import Smiley from '@assets/images/emoji.svg'; +import Flags from '@assets/images/emojiCategoryIcons/flag.svg'; +import FoodAndDrink from '@assets/images/emojiCategoryIcons/hamburger.svg'; +import Objects from '@assets/images/emojiCategoryIcons/light-bulb.svg'; +import Symbols from '@assets/images/emojiCategoryIcons/peace-sign.svg'; +import TravelAndPlaces from '@assets/images/emojiCategoryIcons/plane.svg'; +import AnimalsAndNature from '@assets/images/emojiCategoryIcons/plant.svg'; +import Activities from '@assets/images/emojiCategoryIcons/soccer-ball.svg'; +import FrequentlyUsed from '@assets/images/history.svg'; +import {HeaderEmoji, PickerEmojis} from './types'; const skinTones = [ { @@ -33,9 +34,9 @@ const skinTones = [ code: '🖐🏿', skinTone: 0, }, -]; +] as const; -const emojis = [ +const emojis: PickerEmojis = [ { header: true, icon: Smiley, @@ -7619,7 +7620,7 @@ const emojis = [ }, ]; -const categoryFrequentlyUsed = { +const categoryFrequentlyUsed: HeaderEmoji = { header: true, code: 'frequentlyUsed', icon: FrequentlyUsed, diff --git a/assets/emojis/en.js b/assets/emojis/en.ts similarity index 99% rename from assets/emojis/en.js rename to assets/emojis/en.ts index f32a91afe03c..0a1ca7611117 100644 --- a/assets/emojis/en.js +++ b/assets/emojis/en.ts @@ -1,4 +1,7 @@ -const enEmojis = { +import {EmojisList} from './types'; + +/* eslint-disable @typescript-eslint/naming-convention */ +const enEmojis: EmojisList = { '😀': { keywords: ['smile', 'happy', 'face', 'grin'], }, diff --git a/assets/emojis/es.js b/assets/emojis/es.ts similarity index 99% rename from assets/emojis/es.js rename to assets/emojis/es.ts index fda12f5f127c..46f825643859 100644 --- a/assets/emojis/es.js +++ b/assets/emojis/es.ts @@ -1,4 +1,7 @@ -const esEmojis = { +import {EmojisList} from './types'; + +/* eslint-disable @typescript-eslint/naming-convention */ +const esEmojis: EmojisList = { '😀': { name: 'sonriendo', keywords: ['cara', 'divertido', 'feliz', 'sonrisa', 'cara sonriendo'], diff --git a/assets/emojis/index.js b/assets/emojis/index.js deleted file mode 100644 index c8dab36f57d9..000000000000 --- a/assets/emojis/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import _ from 'underscore'; -import emojis from './common'; -import enEmojis from './en'; -import esEmojis from './es'; - -const emojiNameTable = _.reduce( - emojis, - (prev, cur) => { - const newValue = prev; - if (!cur.header) { - newValue[cur.name] = cur; - } - return newValue; - }, - {}, -); - -const emojiCodeTableWithSkinTones = _.reduce( - emojis, - (prev, cur) => { - const newValue = prev; - if (!cur.header) { - newValue[cur.code] = cur; - } - if (cur.types) { - cur.types.forEach((type) => { - newValue[type] = cur; - }); - } - return newValue; - }, - {}, -); - -const localeEmojis = { - en: enEmojis, - es: esEmojis, -}; - -export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; -export {skinTones, categoryFrequentlyUsed, default} from './common'; diff --git a/assets/emojis/index.ts b/assets/emojis/index.ts new file mode 100644 index 000000000000..aade4e557a64 --- /dev/null +++ b/assets/emojis/index.ts @@ -0,0 +1,35 @@ +import emojis from './common'; +import enEmojis from './en'; +import esEmojis from './es'; +import {Emoji} from './types'; + +type EmojiTable = Record; + +const emojiNameTable = emojis.reduce((prev, cur) => { + const newValue = prev; + if (!('header' in cur) && cur.name) { + newValue[cur.name] = cur; + } + return newValue; +}, {}); + +const emojiCodeTableWithSkinTones = emojis.reduce((prev, cur) => { + const newValue = prev; + if (!('header' in cur)) { + newValue[cur.code] = cur; + } + if ('types' in cur && cur.types) { + cur.types.forEach((type) => { + newValue[type] = cur; + }); + } + return newValue; +}, {}); + +const localeEmojis = { + en: enEmojis, + es: esEmojis, +} as const; + +export {emojiNameTable, emojiCodeTableWithSkinTones, localeEmojis}; +export {skinTones, categoryFrequentlyUsed, default} from './common'; diff --git a/assets/emojis/types.ts b/assets/emojis/types.ts new file mode 100644 index 000000000000..a42f44ed7fa7 --- /dev/null +++ b/assets/emojis/types.ts @@ -0,0 +1,19 @@ +import {SvgProps} from 'react-native-svg'; + +type Emoji = { + code: string; + name: string; + types?: string[]; +}; + +type HeaderEmoji = { + header: true; + icon: React.FC; + code: string; +}; + +type PickerEmojis = Array; + +type EmojisList = Record; + +export type {Emoji, HeaderEmoji, EmojisList, PickerEmojis}; diff --git a/assets/images/bell.svg b/assets/images/bell.svg index 6ba600dc695b..5a6b411185a9 100644 --- a/assets/images/bell.svg +++ b/assets/images/bell.svg @@ -1,6 +1 @@ - - - - - + \ No newline at end of file diff --git a/assets/images/home-background--android.svg b/assets/images/home-background--android.svg index 507aecf04836..2b72b6ccabe9 100644 --- a/assets/images/home-background--android.svg +++ b/assets/images/home-background--android.svg @@ -1,6555 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 7de6926c850d..d8ad66917b82 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,16 +17,8 @@ const defaultPlugins = [ ]; const webpack = { - env: { - production: { - presets: defaultPresets, - plugins: [...defaultPlugins, 'transform-remove-console'], - }, - development: { - presets: defaultPresets, - plugins: defaultPlugins, - }, - }, + presets: defaultPresets, + plugins: defaultPlugins, }; const metro = { @@ -78,6 +70,11 @@ const metro = { }, ], ], + env: { + production: { + plugins: [['transform-remove-console', {exclude: ['error', 'warn']}]], + }, + }, }; /* @@ -102,11 +99,19 @@ if (process.env.CAPTURE_METRICS === 'true') { ]); } -module.exports = ({caller}) => { +module.exports = (api) => { + console.debug('babel.config.js'); + console.debug(' - api.version:', api.version); + console.debug(' - api.env:', api.env()); + console.debug(' - process.env.NODE_ENV:', process.env.NODE_ENV); + console.debug(' - process.env.BABEL_ENV:', process.env.BABEL_ENV); + // For `react-native` (iOS/Android) caller will be "metro" // For `webpack` (Web) caller will be "@babel-loader" // For jest, it will be babel-jest // For `storybook` there won't be any config at all so we must give default argument of an empty object - const runningIn = caller((args = {}) => args.name); + const runningIn = api.caller((args = {}) => args.name); + console.debug(' - running in: ', runningIn); + return ['metro', 'babel-jest'].includes(runningIn) ? metro : webpack; }; diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js index 8a9aadb427b2..22d035368c42 100644 --- a/config/webpack/webpack.common.js +++ b/config/webpack/webpack.common.js @@ -13,7 +13,7 @@ const includeModules = [ 'react-native-animatable', 'react-native-reanimated', 'react-native-picker-select', - '@expensify/react-native-web', + 'react-native-web', 'react-native-webview', '@react-native-picker', 'react-native-modal', @@ -189,8 +189,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({ resolve: { alias: { 'react-native-config': 'react-web-config', - 'react-native$': '@expensify/react-native-web', - 'react-native-web': '@expensify/react-native-web', + 'react-native$': 'react-native-web', // Module alias for web & desktop // https://webpack.js.org/configuration/resolve/#resolvealias diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index 1fb67483daca..ffec5f20254c 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -53,17 +53,27 @@ The phone number can be formatted in different ways. ### Native Keyboards -We should always set people up for success on native platforms by enabling the best keyboard for the type of input we’re asking them to provide. See [keyboardType](https://reactnative.dev/docs/0.64/textinput#keyboardtype) in the React Native documentation. +We should always set people up for success on native platforms by enabling the best keyboard for the type of input we’re asking them to provide. See [inputMode](https://reactnative.dev/docs/textinput#inputmode) in the React Native documentation. -We have a couple of keyboard types [defined](https://github.com/Expensify/App/blob/572caa9e7cf32a2d64fe0e93d171bb05a1dfb217/src/CONST.js#L357-L360) and should be used like so: +We have a list of input modes [defined](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L765-L774) and should be used like so: ```jsx ``` +We also have [keyboardType](https://github.com/Expensify/App/blob/9418b870515102631ea2156b5ea253ee05a98ff1/src/CONST.js#L760-L763) and should be used for specific use cases when there is no `inputMode` equivalent of the value exist. and should be used like so: + +```jsx + +``` + + ### Autofill Behavior Forms should autofill information whenever possible i.e. they should work with browsers and password managers auto complete features. diff --git a/docs/Expensify-Card-revenue share for ExpensifyApproved!-partners.md b/docs/Expensify-Card-revenue share for ExpensifyApproved!-partners.md deleted file mode 100644 index f9d18da76ef6..000000000000 --- a/docs/Expensify-Card-revenue share for ExpensifyApproved!-partners.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Expensify Card revenue share for ExpensifyApproved! partners -description: Earn money when your clients adopt the Expensify Card ---- - - -# About -Start making more with us! We're thrilled to announce a new incentive for our US-based ExpensifyApproved! partner accountants. You can now earn additional income for your firm every time your client uses their Expensify Card. We're offering 0.5% of the total Expensify Card spend of your clients in cashback returned to your firm. The more your clients spend, the more cashback your firm receives!
-
This program is currently only available to US-based ExpensifyApproved! partner accountants. - -# How-to -To benefit from this program, complete the following steps -1. Ensure that you are listed as the Primary Contact under your client's domain in Expensify. If you're not currently the Primary Contact for your client, you or your client can follow the instructions outlined in [our help article](https://community.expensify.com/discussion/5749/how-to-add-and-remove-domain-admins#:~:text=Domain%20Admins%20have%20total%20control,a%20member%20of%20the%20domain.) to assign you this role. -2. Add a Business Bank Account to the Primary Contact account. Follow the instructions in [our help article](https://community.expensify.com/discussion/4641/how-to-add-a-deposit-only-bank-account-both-personal-and-business) to get a Business Bank Account added. - -# FAQ -- What if my firm is not permitted to accept revenue share from our clients?
-
We understand that different firms may have different policies. If your firm is unable to accept this revenue share, you can pass the revenue share back to your client to give them an additional 0.5% of cash back using your own internal payment tools.

-How will I know which clients to pay back?
-
Every month you will receive an automated message via new.expensify.com and email providing a breakdown of revenue shared generated per client.

-- What if my firm does not wish to participate in the program?
-
Please reach out to your assigned partner manager at new.expensify.com to inform them you would not like to accept the revenue share nor do you want to pass the revenue share to your clients. diff --git a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md index 76ebba9ef76b..33ffe7172603 100644 --- a/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md +++ b/docs/articles/expensify-classic/manage-employees-and-report-approvals/Removing-Members.md @@ -11,6 +11,8 @@ Removing a member from a workspace disables their ability to use the workspace. 3. Select the member you'd like to remove and click the **Remove** button at the top of the Members table. 4. If this member was an approver, make sure that reports are not routing to them in the workflow. +![image of members table in a workspace]({{site.url}}/assets/images/ExpensifyHelp_RemovingMembers.png){:width="100%"} + # FAQ ## Will reports from this member on this workspace still be available? diff --git a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md index 0db022f400d3..783bd50f17a3 100644 --- a/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md +++ b/docs/articles/expensify-classic/workspace-and-domain-settings/Categories.md @@ -1,5 +1,153 @@ --- title: Categories -description: Categories +description: Categories are used to classify and organize expenses --- -## Resource Coming Soon! +# Overview +Categories are commonly used to classify and organize expenses for both personal finances and business accounting. + +The way Categories function will vary depending on whether or not you connect Expensify to a direct accounting integration (i.e., QuickBooks Online, NetSuite, etc.). + +When reviewing this resource, be sure to take a look at the section that applies to your account setup! + +# How to use Categories +- When using an accounting integration, categories are your chart of accounts/ show in expense claims/etc. +- You can have different categories for different workspaces. + +# How to import Categories (no accounting integration connected) + +## Add Categories via spreadsheet +If you need to import multiple categories at once, you can upload a spreadsheet of these parameters directly to Expensify. + +Before importing the category spreadsheet to Expensify, you'll want to format it so that it lists the Category name as well as any optional fields you'd like to include. + +Required fields: +- Category name + +Optional fields: +- GL Code +- Payroll code +- Enabled (TRUE/ FALSE) +- Max Expense amount +- Receipt Required +- Comments (Required/ Not Required) +- Comment Hint +- Expense Limit Type + +Expensify supports the following file formats for uploading Categories in bulk: + +- CSV +- TXT +- XLS +- XLSX + +Once the spreadsheet is formatted, you can upload it to the workspace under **Settings > Workspace > Group >** *[Workspace Name]* **> Categories**. + +From there, the updated Category list will show as available on all expenses submitted on the corresponding workspace. + +## Manually add Categories +If you need to add Categories to your workspace manually, you can follow the steps below. + +On web: +1. Navigate to **Settings > Workspace > Group/Individual >** *[Workspace Name]* **> Categories**. +2. Add new categories under **Add a Category**. + +On mobile: +1. Tap the **three-bar menu icon** at the top left corner of the app. +2. Tap on **Settings** in the menu on the left side. +3. Scroll to the Workspace subhead and click on Categories listed underneath the default Workspace. +4. Add new categories by tapping the **+** button in the upper right corner. To delete a category, on iOS swipe left, on Android press and hold. Tap a category name to edit it. + +## Add sub-categories +Sub-categories are useful when you want to be more specific, i.e. Travel could have a subcategory of airplane or lodging so the outcome would be Travel:airplane or Travel:lodging. + +If you would like to create sub-categories under your category selection drop-down list, you can do so by adding a colon after the name of the desired category and then typing the sub-category (without spaces around the punctuation). + +# How to import Categories with an accounting integration connected +If you connect Expensify to a direct integration such as QuickBooks Online, QuickBooks Desktop, Sage Intacct, Xero, or NetSuite, then Expensify automatically imports the general ledger parameters from your accounting system as Categories. + +When you first connect your accounting integration your categories will most likely be pulled from your chart of accounts, however this can vary depending on the account integration. + +If you need to update your categories in Expensify, you will first need to update them in your accounting system, then sync the connection in Expensify by navigating to **Settings > Workspace > Group >** _[Workspace Name]_ **> Connection > Sync Now**. + +Alternatively, if you update the category details in your accounting integration, be sure to sync the workspace connection so that the updated information is available on the workspace. + +# Deep Dive + +## Category-specific rules and description hints +If you're an admin using a workspace on a Control plan, you have the ability to enable specific rules for each category. + +These settings are valuable if you want to set a special limit for a certain category. e.g. Your default expense limit is $2500 but Entertainment has a limit of $150 per person, per day. You can also require or not require receipts, which is great for allowing things like Mileage or Per Diems to have no receipt. + +To set up Category Rules, go to **Settings > Workspace> Group >** _[Workspace Name]_ **> Categories**. + +Then, click **Edit Rules** next to the category name for which you'd like to define a rule. + +- **GL Code and Payroll Code**: These are optional fields if these categories need to be associated with either of these codes in your accounting or payroll systems. GL code will be automatically populated if connecting to an accounting integration. +- **Max Amount**: Allows you to set specific expense amount caps based on the expense category. Using **Limit type**, you can define this **per individual expense**, or **per day** (for expenses in a category on an expense report). +- **Receipts**: Allows you to decide whether you want to require receipts based on the category of the expense. For instance, it's common for companies to disable the receipt requirement for Toll expenses. +- **Description**: Allows you to decide whether to require the `description` field to be filled out based on the category of the expense. +- **Description Hint**: This allows you to place a hint in the `description` field. This will appear in light gray font on the expense edit screen in this field to prompt the expense creator to fill in the field accordingly. +- **Rule Enforcement**: If users are in violation of these rules, those violations will be shown in red on the report. Any category-specific violations will only be shown once a category has been selected for a given expense. + +## Make categories required +This means all expenses must be coded with a Category. If they do not have a category, there will be a violation on the expense which can prevent submission. + + +## Update Category rules via spreadsheet +You can update Category rules en masse by exporting them to CSV, editing the spreadsheet, and then importing them back to the categories page. + +This allows you to quickly add new categories and set GL codes, payroll codes, and description hints. This feature is only available for indirect integration setups. + +## Category approvers +Workspace admins can add additional approvers who must approve any expenses categorized with a particular category. + +To configure category approvers: + +1. Go to Settings > Workspace > Group > _[Select Workspace]_ > Categories +2. Click Edit Rules next to the category name that requires a category approver and use the "Approver" field + +The expense will route to this Category approver, before following the approval workflow set up in the People table + +## Auto-categorize card expenses with default categories + +If you're importing card transactions, **Default Categorization** will provide a massive benefit to your company's workflow by **automatically coding** expenses to the proper GL. +- Once configured according to your GL, the default category will detect the type of merchant for an expense based on its Merchant Category Code (MCC) and associate that expense with the proper GL account. +- This time-saver keeps employees from having to manually code expenses and provides admins with the peace of mind that the expenses coming in for approval are more reliably associated with the correct GL account. +- Best of all, this works for personal and company cards alike! + +## Setting up Default Categories + +1. First, go to **Settings** and select the **group Workspace** that you want to configure. +2. Go to the **Categories** tab and scroll down to **Default Categories**. +3. Under the **Category** column, select the account that is most closely associated with the merchant group for all groups that apply to expenses that you and your coworkers submit. If you are unsure, just leave the group **Uncategorized** and the expense will not come in pre-categorized. +4. You're well on your way to expense automation freedom! + +## Default Categories based on specific MCC codes + +If you require more granular detail, the MCC Editor gives you even greater control over which MCC Codes are assigned to which Categories. The MCC Editor can be found just below the Default Categories table. + +## Implicit categorization +Over time, Expensify will learn how you categorize certain merchants and then automatically apply that category to the same merchant in the future. + +- You can always change the category; we'll try to remember that correction for next time! +- Any Expense Rules you have set up will always take precedence over implicit categories. +- Implicit categorization will **only** apply to expenses if you have **not** explicitly set a category already. Changing the category on one expense does not change it for any other expense that has an explicit category already assigned. + +This built-in feature will only use the categories from the currently active workspace on your account. You can change the active workspace by clicking your account icon in the app and selecting the correct workspace name before you SmartScan. + +## Category violations +Category violations can happen for the following reasons: + +- An employee categorized an expense with a category not included in the workspace's categories. This would throw a "category out of workspace" violation. +- If you change your categories importing from an accounting integration, this can cause an old category to still be in use on an open report which would throw a violation on submission. Simply reselect a proper category to clear violation. + +If Scheduled Submit is enabled on a workspace, expenses with category violations will not be auto-submitted unless the expense has a comment added. + +# FAQ + +## The correct category list isn't showing when one of my employees is categorizing their expenses. Why is this happening? +Its possible the employee is defaulted to their personal workspace so the expenses are not pulling the correct categories to choose from. Check to be sure the report is listed under the correct workspace by looking under the details section on top right of report. + +## Will the account numbers from our accounting system (QuickBooks Online, Sage Intacct, etc.) show in the Category list when employees are choosing what chart of accounts category to code their expense to? +The GL account numbers will be visible in the workspace settings when connected to a Control-level workspace for workspace admins to see. We do not provide this information in an employee-facing capacity because most employees do not have access to that information within the accounting integration. +If you wish to have this information available to your employees when they are categorizing their expenses, you can edit the account name in your accounting software to include the GL number — i.e. **Accounts Payable - 12345** diff --git a/docs/assets/images/ExpensifyHelp_RemovingMembers.png b/docs/assets/images/ExpensifyHelp_RemovingMembers.png new file mode 100644 index 000000000000..1e0157476fbf Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_RemovingMembers.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv new file mode 100644 index 000000000000..42e5bd005cae --- /dev/null +++ b/docs/redirects.csv @@ -0,0 +1,8 @@ +sourceURL,targetURL +https://community.expensify.com/discussion/5634/deep-dive-how-long-will-it-take-for-me-to-receive-my-reimbursement,https://help.expensify.com/articles/expensify-classic/get-paid-back/reports/Reimbursements +https://community.expensify.com/discussion/4925/how-to-dispute-an-expensify-card-transaction,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction#gsc.tab=0 +https://community.expensify.com/discussion/5184/faq-how-am-i-protected-from-fraud-using-the-expensify-card,https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction#gsc.tab=0 +https://community.expensify.com/discussion/4887/deep-dive-understanding-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/4883/how-to-export-your-expensify-card-statement,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/9528/how-to-understand-the-amount-owed-figure,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 +https://community.expensify.com/discussion/4806/how-to-pay-your-expensify-card-balance,https://help.expensify.com/articles/expensify-classic/expensify-card/Statements#gsc.tab=0 diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 94d2986fd111..dffa6d42e2ed 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.96 + 1.3.97 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.96.0 + 1.3.97.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9478336965cf..f69808a8fff5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.96 + 1.3.97 CFBundleSignature ???? CFBundleVersion - 1.3.96.0 + 1.3.97.1 diff --git a/package-lock.json b/package-lock.json index d80dd7504a28..83186f120cc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,16 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.97-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.97-1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-web": "0.18.15", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", @@ -52,7 +51,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#82bfcd1cb077afd03d1c8c069618c7dd5bd405d8", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a055da6a339383da825f353d5c0da72a26285007", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -95,7 +94,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.111", + "react-native-onyx": "1.0.115", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -113,6 +112,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "^3.6.0", "react-native-vision-camera": "^2.16.2", + "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "^11.17.2", "react-pdf": "^6.2.2", @@ -139,6 +139,7 @@ "@dword-design/eslint-plugin-import-alias": "^4.0.8", "@electron/notarize": "^2.1.0", "@jest/globals": "^29.5.0", + "@ngneat/falso": "^7.1.1", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", @@ -3683,25 +3684,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@expensify/react-native-web": { - "version": "0.18.15", - "resolved": "https://registry.npmjs.org/@expensify/react-native-web/-/react-native-web-0.18.15.tgz", - "integrity": "sha512-xE3WdGKY4SRLfIrimUlgP78ZsDaWy3g+KIO8mpxTm9zCXeX/sgEYs6QvhFghgEhhp7Y1bLH9LWTKiZy9LZM8EA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", - "fbjs": "^3.0.4", - "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", - "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" - }, - "peerDependencies": { - "react": "^17.0.2 || ^18.0.0", - "react-dom": "^17.0.2 || ^18.0.0" - } - }, "node_modules/@expo/config-plugins": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-4.1.5.tgz", @@ -6022,6 +6004,16 @@ "@types/react-native": "*" } }, + "node_modules/@ngneat/falso": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@ngneat/falso/-/falso-7.1.1.tgz", + "integrity": "sha512-/5HuJDaZHXl3WVdgvYBAM52OSYbSKfiNazVOZOw/3KjeZ6dQW9F0QCG+W6z52lUu5MZvp/TkPGaVRtoz6h9T1w==", + "dev": true, + "dependencies": { + "seedrandom": "3.0.5", + "uuid": "8.3.2" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -26263,16 +26255,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "node_modules/cross-fetch": { "version": "3.1.5", "license": "MIT", @@ -30347,8 +30329,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#82bfcd1cb077afd03d1c8c069618c7dd5bd405d8", - "integrity": "sha512-Pvji3XqRyCbhgaKLVPT0HfRl/cazGStQeo8V6tWcU1n3UNiG/6Qey4jAdfN8WQZlfslrSzFiZTWrD7UT0JeRrQ==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a055da6a339383da825f353d5c0da72a26285007", + "integrity": "sha512-3szmT0160oahs8DMBT7kBBhz/FKofRfZ59q9E2/0ktBAeP4wx+zp13aUenGueabq7S/BPAidBni7tUrKpF0SIA==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -41731,12 +41713,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/normalize-css-color": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz", - "integrity": "sha512-jPJ/V7Cp1UytdidsPqviKEElFQJs22hUUgK5BOPHTwOonNCk7/2qOxhhqzEajmFrWJowADFfOFh1V+aWkRfy+w==", - "license": "BSD-3-Clause" - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -44805,13 +44781,13 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.111", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz", - "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==", + "version": "1.0.115", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.115.tgz", + "integrity": "sha512-uPrJcw3Ta/EFL3Mh3iUggZ7EeEwLTSSSc5iUkKAA+a9Y8kBo8+6MWup9VCM/4wgysZbf3VHUGJCWQ8H3vWKgUg==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.6" }, "engines": { "node": ">=16.15.1 <=18.17.1", @@ -45109,7 +45085,6 @@ "version": "0.19.9", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-color": "^2.1.0", @@ -45136,8 +45111,7 @@ "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "peer": true + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, "node_modules/react-native-webview": { "version": "11.23.0", @@ -55662,20 +55636,6 @@ } } }, - "@expensify/react-native-web": { - "version": "0.18.15", - "resolved": "https://registry.npmjs.org/@expensify/react-native-web/-/react-native-web-0.18.15.tgz", - "integrity": "sha512-xE3WdGKY4SRLfIrimUlgP78ZsDaWy3g+KIO8mpxTm9zCXeX/sgEYs6QvhFghgEhhp7Y1bLH9LWTKiZy9LZM8EA==", - "requires": { - "@babel/runtime": "^7.18.6", - "create-react-class": "^15.7.0", - "fbjs": "^3.0.4", - "inline-style-prefixer": "^6.0.1", - "normalize-css-color": "^1.0.2", - "postcss-value-parser": "^4.2.0", - "styleq": "^0.1.2" - } - }, "@expo/config-plugins": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-4.1.5.tgz", @@ -57420,6 +57380,16 @@ "csstype": "^3.0.8" } }, + "@ngneat/falso": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@ngneat/falso/-/falso-7.1.1.tgz", + "integrity": "sha512-/5HuJDaZHXl3WVdgvYBAM52OSYbSKfiNazVOZOw/3KjeZ6dQW9F0QCG+W6z52lUu5MZvp/TkPGaVRtoz6h9T1w==", + "dev": true, + "requires": { + "seedrandom": "3.0.5", + "uuid": "8.3.2" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -72112,15 +72082,6 @@ "sha.js": "^2.4.8" } }, - "create-react-class": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", - "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, "cross-fetch": { "version": "3.1.5", "requires": { @@ -75065,9 +75026,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#82bfcd1cb077afd03d1c8c069618c7dd5bd405d8", - "integrity": "sha512-Pvji3XqRyCbhgaKLVPT0HfRl/cazGStQeo8V6tWcU1n3UNiG/6Qey4jAdfN8WQZlfslrSzFiZTWrD7UT0JeRrQ==", - "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#82bfcd1cb077afd03d1c8c069618c7dd5bd405d8", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#a055da6a339383da825f353d5c0da72a26285007", + "integrity": "sha512-3szmT0160oahs8DMBT7kBBhz/FKofRfZ59q9E2/0ktBAeP4wx+zp13aUenGueabq7S/BPAidBni7tUrKpF0SIA==", + "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#a055da6a339383da825f353d5c0da72a26285007", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -83172,11 +83133,6 @@ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==" }, - "normalize-css-color": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/normalize-css-color/-/normalize-css-color-1.0.2.tgz", - "integrity": "sha512-jPJ/V7Cp1UytdidsPqviKEElFQJs22hUUgK5BOPHTwOonNCk7/2qOxhhqzEajmFrWJowADFfOFh1V+aWkRfy+w==" - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -85423,13 +85379,13 @@ } }, "react-native-onyx": { - "version": "1.0.111", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.111.tgz", - "integrity": "sha512-6drd5Grhkyq4oyt2+Bu6t7JYK5tqaARc0YP7taEHK9jLbhjdC4E9MPLJR2FVXiORkQCPOoyy1Gqmb4AUVIsvxg==", + "version": "1.0.115", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.115.tgz", + "integrity": "sha512-uPrJcw3Ta/EFL3Mh3iUggZ7EeEwLTSSSc5iUkKAA+a9Y8kBo8+6MWup9VCM/4wgysZbf3VHUGJCWQ8H3vWKgUg==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", - "underscore": "^1.13.1" + "underscore": "^1.13.6" } }, "react-native-pager-view": { @@ -85610,7 +85566,6 @@ "version": "0.19.9", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.9.tgz", "integrity": "sha512-m69arZbS6FV+BNSKE6R/NQwUX+CzxCkYM7AJlSLlS8dz3BDzlaxG8Bzqtzv/r3r1YFowhnZLBXVKIwovKDw49g==", - "peer": true, "requires": { "@babel/runtime": "^7.18.6", "@react-native/normalize-color": "^2.1.0", @@ -85625,8 +85580,7 @@ "memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", - "peer": true + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" } } }, diff --git a/package.json b/package.json index 51f56d773e05..8d5988971c89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.96-0", + "version": "1.3.97-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -60,7 +60,6 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-web": "0.18.15", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-getcanonicallocales": "^2.2.0", "@formatjs/intl-listformat": "^7.2.2", @@ -101,7 +100,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#82bfcd1cb077afd03d1c8c069618c7dd5bd405d8", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a055da6a339383da825f353d5c0da72a26285007", "fbjs": "^3.0.2", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -144,7 +143,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.111", + "react-native-onyx": "1.0.115", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -162,6 +161,7 @@ "react-native-url-polyfill": "^2.0.0", "react-native-view-shot": "^3.6.0", "react-native-vision-camera": "^2.16.2", + "react-native-web": "^0.19.9", "react-native-web-linear-gradient": "^1.1.2", "react-native-webview": "^11.17.2", "react-pdf": "^6.2.2", @@ -188,6 +188,7 @@ "@dword-design/eslint-plugin-import-alias": "^4.0.8", "@electron/notarize": "^2.1.0", "@jest/globals": "^29.5.0", + "@ngneat/falso": "^7.1.1", "@octokit/core": "4.0.4", "@octokit/plugin-paginate-rest": "3.1.0", "@octokit/plugin-throttling": "4.1.0", diff --git a/patches/eslint-plugin-react-native-a11y+3.3.0.patch b/patches/eslint-plugin-react-native-a11y+3.3.0.patch new file mode 100644 index 000000000000..fe5998118afa --- /dev/null +++ b/patches/eslint-plugin-react-native-a11y+3.3.0.patch @@ -0,0 +1,59 @@ +diff --git a/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js b/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js +index 9ecf8b1..fef94dd 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js ++++ b/node_modules/eslint-plugin-react-native-a11y/__tests__/src/rules/has-valid-accessibility-descriptors-test.js +@@ -20,7 +20,7 @@ const ruleTester = new RuleTester(); + + const expectedError = { + message: +- 'Missing a11y props. Expected one of: accessibilityRole OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction', ++ 'Missing a11y props. Expected one of: accessibilityRole OR role OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction', + type: 'JSXOpeningElement', + }; + +@@ -29,6 +29,11 @@ ruleTester.run('has-valid-accessibility-descriptors', rule, { + { + code: ';', + }, ++ { ++ code: ` ++ Back ++ `, ++ }, + { + code: ` + Back +diff --git a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js +index 99deb91..555ebd9 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js ++++ b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-descriptors.js +@@ -16,7 +16,7 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de + // ---------------------------------------------------------------------------- + // Rule Definition + // ---------------------------------------------------------------------------- +-var errorMessage = 'Missing a11y props. Expected one of: accessibilityRole OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction'; ++var errorMessage = 'Missing a11y props. Expected one of: accessibilityRole OR role OR BOTH accessibilityLabel + accessibilityHint OR BOTH accessibilityActions + onAccessibilityAction'; + var schema = (0, _schemas.generateObjSchema)(); + + var hasSpreadProps = function hasSpreadProps(attributes) { +@@ -35,7 +35,7 @@ module.exports = { + return { + JSXOpeningElement: function JSXOpeningElement(node) { + if ((0, _isTouchable.default)(node, context) || (0, _jsxAstUtils.elementType)(node) === 'TextInput') { +- if (!(0, _jsxAstUtils.hasAnyProp)(node.attributes, ['accessibilityRole', 'accessibilityLabel', 'accessibilityActions', 'accessible']) && !hasSpreadProps(node.attributes)) { ++ if (!(0, _jsxAstUtils.hasAnyProp)(node.attributes, ['role', 'accessibilityRole', 'accessibilityLabel', 'accessibilityActions', 'accessible']) && !hasSpreadProps(node.attributes)) { + context.report({ + node, + message: errorMessage, +diff --git a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js +index fe74702..fa6bdaa 100644 +--- a/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js ++++ b/node_modules/eslint-plugin-react-native-a11y/lib/rules/has-valid-accessibility-role.js +@@ -13,5 +13,5 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de + // Rule Definition + // ---------------------------------------------------------------------------- + var errorMessage = 'accessibilityRole must be one of defined values'; +-var validValues = ['togglebutton', 'adjustable', 'alert', 'button', 'checkbox', 'combobox', 'header', 'image', 'imagebutton', 'keyboardkey', 'link', 'menu', 'menubar', 'menuitem', 'none', 'progressbar', 'radio', 'radiogroup', 'scrollbar', 'search', 'spinbutton', 'summary', 'switch', 'tab', 'tabbar', 'tablist', 'text', 'timer', 'list', 'toolbar']; ++var validValues = ['img', 'img button', 'img link', 'togglebutton', 'adjustable', 'alert', 'button', 'checkbox', 'combobox', 'header', 'image', 'imagebutton', 'keyboardkey', 'link', 'menu', 'menubar', 'menuitem', 'none', 'progressbar', 'radio', 'radiogroup', 'scrollbar', 'search', 'spinbutton', 'summary', 'switch', 'tab', 'tabbar', 'tablist', 'text', 'timer', 'list', 'toolbar']; + module.exports = (0, _validProp.default)('accessibilityRole', validValues, errorMessage); +\ No newline at end of file diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index 049a7a09d16a..5bfb2cc5f0b0 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..a88a2ca 100644 +index 80f4e75..3ba8b8c 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js @@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { @@ -28,23 +28,27 @@ index 80f4e75..a88a2ca 100644 this.shouldPropagateSwipe = (evt, gestureState) => { return typeof this.props.propagateSwipe === 'function' ? this.props.propagateSwipe(evt, gestureState) -@@ -454,9 +461,15 @@ export class ReactNativeModal extends React.Component { +@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component { + if (this.state.isVisible) { this.open(); } - BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPress); + if (Platform.OS === 'web') { + document?.body?.addEventListener?.('keyup', this.handleEscape, true); ++ return; + } + BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPress); } componentWillUnmount() { - BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); +- BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); + if (Platform.OS === 'web') { + document?.body?.removeEventListener?.('keyup', this.handleEscape, true); ++ } else { ++ BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); + } if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } -@@ -525,7 +538,7 @@ export class ReactNativeModal extends React.Component { +@@ -525,7 +540,7 @@ export class ReactNativeModal extends React.Component { } return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), this.makeBackdrop(), diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch new file mode 100644 index 000000000000..d88ef83d4bcd --- /dev/null +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -0,0 +1,286 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index c879838..288316c 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { + * + */ + class VirtualizedList extends StateSafePureComponent { ++ pushOrUnshift(input, item) { ++ if (this.props.inverted) { ++ input.unshift(item); ++ } else { ++ input.push(item); ++ } ++ } ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params) { + var animated = params ? params.animated : true; +@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._defaultRenderScrollComponent = props => { + var onRefresh = props.onRefresh; ++ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return /*#__PURE__*/React.createElement(View, props); +@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { + refreshing: props.refreshing, + onRefresh: onRefresh, + progressViewOffset: props.progressViewOffset +- }) : props.refreshControl ++ }) : props.refreshControl, ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] + })) + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return /*#__PURE__*/React.createElement(ScrollView, props); ++ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] ++ })); + } + }; + this._onCellLayout = (e, cellKey, index) => { +@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { + onViewableItemsChanged = _this$props3.onViewableItemsChanged, + viewabilityConfig = _this$props3.viewabilityConfig; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged + }); +@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { + var key = _this._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- stickyHeaderIndices.push(cells.length); ++ _this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); +- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ ++ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ + CellRendererComponent: CellRendererComponent, + ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, + ListItemComponent: ListItemComponent, +@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : + /*#__PURE__*/ + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListHeaderComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" + }, /*#__PURE__*/React.createElement(View, { +@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListEmptyComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-empty', + key: "$empty" + }, /*#__PURE__*/React.cloneElement(_element2, { +@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { + var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); + var lastMetrics = this.__getFrameMetricsApprox(last, this.props); + var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( /*#__PURE__*/React.createElement(View, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { + key: "$spacer-" + section.first, + style: { + [spacerKey]: spacerSize +@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListFooterComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getFooterCellKey(), + key: "$footer" + }, /*#__PURE__*/React.createElement(View, { +@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } + } + var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; +@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ + left: 0, + borderColor: 'red', + borderWidth: 2 ++ }, ++ rowReverse: { ++ flexDirection: 'row-reverse' ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse' + } + }); + export default VirtualizedList; +\ No newline at end of file +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index c7d68bb..46b3fc9 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -167,6 +167,14 @@ function findLastWhere( + class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; + ++ pushOrUnshift(input: Array, item: Item) { ++ if (this.props.inverted) { ++ input.unshift(item) ++ } else { ++ input.push(item) ++ } ++ } ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; +@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + } else { + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); +@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- stickyHeaderIndices.push(cells.length); ++ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); + } + + const shouldListenForLayout = + getItemLayout == null || debug || this._fillRateHelper.enabled(); + +- cells.push( ++ this.pushOrUnshift(cells, + { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent +@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + )): any); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( ++ this.pushOrUnshift(cells, + { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; ++ const inversionStyle = this.props.inverted ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; ++ + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { + props.refreshControl + ) + } ++ contentContainerStyle={[ ++ inversionStyle, ++ this.props.contentContainerStyle, ++ ]} + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return ; ++ return ( ++ ++ ); + } + }; + +@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } + } + const windowTop = this.__getFrameMetricsApprox( +@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ + borderColor: 'red', + borderWidth: 2, + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + }); + + export default VirtualizedList; +\ No newline at end of file diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch new file mode 100644 index 000000000000..afd681bba3b0 --- /dev/null +++ b/patches/react-native-web+0.19.9+002+fix-mvcp.patch @@ -0,0 +1,687 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index a6fe142..faeb323 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { + var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; + var isEventTargetScrollable = scrollLength > clientLength; + var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; +- var leftoverDelta = delta; ++ var leftoverDelta = delta * 0.5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); + } +@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; + } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount + }; + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + _this.pushOrUnshift(stickyHeaderIndices, cells.length); +@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { + this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { + } + } + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index d896fb1..f303b31 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { + const delta = this.props.horizontal + ? ev.deltaX || ev.wheelDeltaX + : ev.deltaY || ev.wheelDeltaY; +- let leftoverDelta = delta; ++ let leftoverDelta = delta * 5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 + ? Math.min(delta + scrollOffset, 0) +@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } ++ + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); ++ this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + + const shouldListenForLayout = +@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; + ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last, ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last), + }; + } + +@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { + cellKey={this._getCellKey() + '-header'} + key="$header"> + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + const inversionStyle = this.props.inverted +- ? this.props.horizontal +- ? styles.rowReverse +- : styles.columnReverse +- : null; +- ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, +@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/src/App.js b/src/App.js index bff766c1235f..698dfe4437b2 100644 --- a/src/App.js +++ b/src/App.js @@ -24,7 +24,6 @@ import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; -import {SidebarNavigationContextProvider} from './pages/home/sidebar/SidebarNavigationContext'; import ThemeProvider from './styles/themes/ThemeProvider'; import ThemeStylesProvider from './styles/ThemeStylesProvider'; @@ -65,7 +64,6 @@ function App() { EnvironmentProvider, ThemeProvider, ThemeStylesProvider, - SidebarNavigationContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index de902931ffd8..ce9329d909ae 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -869,14 +869,19 @@ const CONST = { RECOVERY_CODE_LENGTH: 8, KEYBOARD_TYPE: { - PHONE_PAD: 'phone-pad', - NUMBER_PAD: 'number-pad', - DECIMAL_PAD: 'decimal-pad', VISIBLE_PASSWORD: 'visible-password', - EMAIL_ADDRESS: 'email-address', ASCII_CAPABLE: 'ascii-capable', + }, + + INPUT_MODE: { + NONE: 'none', + TEXT: 'text', + DECIMAL: 'decimal', + NUMERIC: 'numeric', + TEL: 'tel', + SEARCH: 'search', + EMAIL: 'email', URL: 'url', - DEFAULT: 'default', }, YOUR_LOCATION_TEXT: 'Your Location', @@ -1331,6 +1336,8 @@ const CONST = { SPECIAL_CHAR: /[,/?"{}[\]()&^%;`$=#<>!*]/g, + FIRST_SPACE: /.+?(?=\s)/, + get SPECIAL_CHAR_OR_EMOJI() { return new RegExp(`[~\\n\\s]|(_\\b(?!$))|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu'); }, @@ -2698,13 +2705,13 @@ const CONST = { BUTTON: 'button', LINK: 'link', MENUITEM: 'menuitem', - TEXT: 'text', + TEXT: 'presentation', RADIO: 'radio', - IMAGEBUTTON: 'imagebutton', + IMAGEBUTTON: 'img button', CHECKBOX: 'checkbox', SWITCH: 'switch', - ADJUSTABLE: 'adjustable', - IMAGE: 'image', + ADJUSTABLE: 'slider', + IMAGE: 'img', }, TRANSLATION_KEYS: { ATTACHMENT: 'common.attachment', @@ -2775,12 +2782,10 @@ const CONST = { DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', }, - ONYX_UPDATE_TYPES: { HTTPS: 'https', PUSHER: 'pusher', }, - EVENTS: { SCROLLING: 'scrolling', }, @@ -2799,13 +2804,6 @@ const CONST = { FOOTER: 'footer', }, - GLOBAL_NAVIGATION_OPTION: { - HOME: 'home', - CHATS: 'chats', - SPEND: 'spend', - WORKSPACES: 'workspaces', - }, - MISSING_TRANSLATION: 'MISSING TRANSLATION', SEARCH_MAX_LENGTH: 500, diff --git a/src/Expensify.js b/src/Expensify.js index b7e3f0f60567..1b692f86a197 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -90,6 +90,8 @@ const defaultProps = { isCheckingPublicRoom: true, }; +const SplashScreenHiddenContext = React.createContext({}); + function Expensify(props) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); @@ -105,6 +107,14 @@ function Expensify(props) { }, [props.isCheckingPublicRoom]); const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); + + const contextValue = useMemo( + () => ({ + isSplashHidden, + }), + [isSplashHidden], + ); + const shouldInit = isNavigationReady && (!isAuthenticated || props.isSidebarLoaded) && hasAttemptedToOpenPublicRoom; const shouldHideSplash = shouldInit && !isSplashHidden; @@ -216,10 +226,12 @@ function Expensify(props) { {hasAttemptedToOpenPublicRoom && ( - + + + )} {shouldHideSplash && } @@ -251,3 +263,5 @@ export default compose( }, }), )(Expensify); + +export {SplashScreenHiddenContext}; diff --git a/src/GLOBAL_NAVIGATION_MAPPING.ts b/src/GLOBAL_NAVIGATION_MAPPING.ts deleted file mode 100644 index f879c508ff31..000000000000 --- a/src/GLOBAL_NAVIGATION_MAPPING.ts +++ /dev/null @@ -1,9 +0,0 @@ -import CONST from './CONST'; -import SCREENS from './SCREENS'; - -export default { - [CONST.GLOBAL_NAVIGATION_OPTION.HOME]: [SCREENS.HOME_OLDDOT], - [CONST.GLOBAL_NAVIGATION_OPTION.CHATS]: [SCREENS.REPORT], - [CONST.GLOBAL_NAVIGATION_OPTION.SPEND]: [SCREENS.EXPENSES_OLDDOT, SCREENS.REPORTS_OLDDOT, SCREENS.INSIGHTS_OLDDOT], - [CONST.GLOBAL_NAVIGATION_OPTION.WORKSPACES]: [SCREENS.INDIVIDUAL_WORKSPACES_OLDDOT, SCREENS.GROUPS_WORKSPACES_OLDDOT, SCREENS.CARDS_AND_DOMAINS_OLDDOT], -} as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 8de77ff678a5..fa72a99b5fa2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -275,42 +275,72 @@ const ONYXKEYS = { /** List of Form ids */ FORMS: { ADD_DEBIT_CARD_FORM: 'addDebitCardForm', + ADD_DEBIT_CARD_FORM_DRAFT: 'addDebitCardFormDraft', WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', + WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', WORKSPACE_RATE_AND_UNIT_FORM: 'workspaceRateAndUnitForm', + WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', CLOSE_ACCOUNT_FORM: 'closeAccount', + CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft', PROFILE_SETTINGS_FORM: 'profileSettingsForm', + PROFILE_SETTINGS_FORM_DRAFT: 'profileSettingsFormDraft', DISPLAY_NAME_FORM: 'displayNameForm', + DISPLAY_NAME_FORM_DRAFT: 'displayNameFormDraft', ROOM_NAME_FORM: 'roomNameForm', + ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft', WELCOME_MESSAGE_FORM: 'welcomeMessageForm', + WELCOME_MESSAGE_FORM_DRAFT: 'welcomeMessageFormDraft', LEGAL_NAME_FORM: 'legalNameForm', + LEGAL_NAME_FORM_DRAFT: 'legalNameFormDraft', WORKSPACE_INVITE_MESSAGE_FORM: 'workspaceInviteMessageForm', + WORKSPACE_INVITE_MESSAGE_FORM_DRAFT: 'workspaceInviteMessageFormDraft', DATE_OF_BIRTH_FORM: 'dateOfBirthForm', + DATE_OF_BIRTH_FORM_DRAFT: 'dateOfBirthFormDraft', HOME_ADDRESS_FORM: 'homeAddressForm', + HOME_ADDRESS_FORM_DRAFT: 'homeAddressFormDraft', NEW_ROOM_FORM: 'newRoomForm', + NEW_ROOM_FORM_DRAFT: 'newRoomFormDraft', ROOM_SETTINGS_FORM: 'roomSettingsForm', + ROOM_SETTINGS_FORM_DRAFT: 'roomSettingsFormDraft', NEW_TASK_FORM: 'newTaskForm', + NEW_TASK_FORM_DRAFT: 'newTaskFormDraft', EDIT_TASK_FORM: 'editTaskForm', + EDIT_TASK_FORM_DRAFT: 'editTaskFormDraft', MONEY_REQUEST_DESCRIPTION_FORM: 'moneyRequestDescriptionForm', + MONEY_REQUEST_DESCRIPTION_FORM_DRAFT: 'moneyRequestDescriptionFormDraft', MONEY_REQUEST_MERCHANT_FORM: 'moneyRequestMerchantForm', + MONEY_REQUEST_MERCHANT_FORM_DRAFT: 'moneyRequestMerchantFormDraft', MONEY_REQUEST_AMOUNT_FORM: 'moneyRequestAmountForm', + MONEY_REQUEST_AMOUNT_FORM_DRAFT: 'moneyRequestAmountFormDraft', MONEY_REQUEST_DATE_FORM: 'moneyRequestCreatedForm', + MONEY_REQUEST_DATE_FORM_DRAFT: 'moneyRequestCreatedFormDraft', NEW_CONTACT_METHOD_FORM: 'newContactMethodForm', + NEW_CONTACT_METHOD_FORM_DRAFT: 'newContactMethodFormDraft', WAYPOINT_FORM: 'waypointForm', WAYPOINT_FORM_DRAFT: 'waypointFormDraft', SETTINGS_STATUS_SET_FORM: 'settingsStatusSetForm', + SETTINGS_STATUS_SET_FORM_DRAFT: 'settingsStatusSetFormDraft', SETTINGS_STATUS_CLEAR_AFTER_FORM: 'settingsStatusClearAfterForm', + SETTINGS_STATUS_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusClearAfterFormDraft', SETTINGS_STATUS_SET_CLEAR_AFTER_FORM: 'settingsStatusSetClearAfterForm', + SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusSetClearAfterFormDraft', PRIVATE_NOTES_FORM: 'privateNotesForm', + PRIVATE_NOTES_FORM_DRAFT: 'privateNotesFormDraft', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', + I_KNOW_A_TEACHER_FORM_DRAFT: 'iKnowTeacherFormDraft', INTRO_SCHOOL_PRINCIPAL_FORM: 'introSchoolPrincipalForm', + INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT: 'introSchoolPrincipalFormDraft', REPORT_PHYSICAL_CARD_FORM: 'requestPhysicalCardForm', + REPORT_PHYSICAL_CARD_FORM_DRAFT: 'requestPhysicalCardFormDraft', REPORT_VIRTUAL_CARD_FRAUD: 'reportVirtualCardFraudForm', + REPORT_VIRTUAL_CARD_FRAUD_DRAFT: 'reportVirtualCardFraudFormDraft', }, } as const; type OnyxKeysMap = typeof ONYXKEYS; type OnyxCollectionKey = ValueOf; type OnyxKey = DeepValueOf>; +type OnyxFormKey = ValueOf | OnyxKeysMap['REIMBURSEMENT_ACCOUNT'] | OnyxKeysMap['REIMBURSEMENT_ACCOUNT_DRAFT']; type OnyxValues = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; @@ -411,36 +441,68 @@ type OnyxValues = { // Forms [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM]: OnyxTypes.AddDebitCardForm; + [ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM_DRAFT]: OnyxTypes.AddDebitCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.DISPLAY_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ROOM_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WELCOME_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.LEGAL_NAME_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM]: OnyxTypes.DateOfBirthForm; + [ONYXKEYS.FORMS.DATE_OF_BIRTH_FORM_DRAFT]: OnyxTypes.DateOfBirthForm; [ONYXKEYS.FORMS.HOME_ADDRESS_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.HOME_ADDRESS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_ROOM_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_ROOM_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.ROOM_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_TASK_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_TASK_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.EDIT_TASK_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.EDIT_TASK_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.MONEY_REQUEST_DESCRIPTION_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.MONEY_REQUEST_MERCHANT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.MONEY_REQUEST_AMOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.WAYPOINT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WAYPOINT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.I_KNOW_A_TEACHER_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.INTRO_SCHOOL_PRINCIPAL_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM_DRAFT]: OnyxTypes.Form; }; type OnyxKeyValue = OnyxEntry; export default ONYXKEYS; -export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue}; +export type {OnyxKey, OnyxCollectionKey, OnyxValues, OnyxKeyValue, OnyxFormKey, OnyxKeysMap}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 864e8934ad88..ed9cc6ae987c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -363,17 +363,4 @@ export default { SAASTR: 'saastr', SBE: 'sbe', MONEY2020: 'money2020', - - // Iframe screens from olddot - HOME_OLDDOT: 'home', - - // Spend tab - EXPENSES_OLDDOT: 'expenses', - REPORTS_OLDDOT: 'reports', - INSIGHTS_OLDDOT: 'insights', - - // Workspaces tab - INDIVIDUALS_OLDDOT: 'individual_workspaces', - GROUPS_OLDDOT: 'group_workspaces', - CARDS_AND_DOMAINS_OLDDOT: 'cards-and-domains', } as const; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8ef787edec2e..f7de8cfab4b6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,17 +27,4 @@ export default { SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', - - // Iframe screens from olddot - HOME_OLDDOT: 'Home_OLDDOT', - - // Spend tab - EXPENSES_OLDDOT: 'Expenses_OLDDOT', - REPORTS_OLDDOT: 'Reports_OLDDOT', - INSIGHTS_OLDDOT: 'Insights_OLDDOT', - - // Workspaces tab - INDIVIDUAL_WORKSPACES_OLDDOT: 'IndividualWorkspaces_OLDDOT', - GROUPS_WORKSPACES_OLDDOT: 'GroupWorkspaces_OLDDOT', - CARDS_AND_DOMAINS_OLDDOT: 'CardsAndDomains_OLDDOT', } as const; diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index 5899e68bedb3..43fd5e6a1b98 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -50,11 +50,11 @@ function AmountTextInput(props) { ref={props.forwardedRef} value={props.formattedAmount} placeholder={props.placeholder} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + inputMode={CONST.INPUT_MODE.NUMERIC} blurOnSubmit={false} selection={props.selection} onSelectionChange={props.onSelectionChange} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} + role={CONST.ACCESSIBILITY_ROLE.TEXT} onKeyPress={props.onKeyPress} /> ); diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index fd6c3d358a33..1e2d18bc4691 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -58,7 +58,7 @@ function BaseAnchorForAttachmentsOnly(props) { onPressOut={props.onPressOut} onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} accessibilityLabel={fileName} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > (linkRef = el)} style={StyleSheet.flatten([style, defaultTextStyle])} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.LINK} + role={CONST.ACCESSIBILITY_ROLE.LINK} hrefAttrs={{ rel, target: isEmail || !linkProps.href ? '_self' : target, diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 8231dd7c4fe2..8fcf765ef7c5 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -443,16 +443,21 @@ function AttachmentModal(props) { ) : ( Boolean(sourceForAttachmentView) && shouldLoadAttachment && ( - + // We need the following View component on android native + // So that the event will propagate properly and + // the Password protected preview will be shown for pdf attachement we are about to send. + + + ) )} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index 53a8606c927f..38f70057be61 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -61,8 +61,7 @@ function CarouselItem({item, isFocused, onPress}) { onPress={() => setIsHidden(!isHidden)} > {isHidden ? translate('moderation.revealMessage') : translate('moderation.hideMessage')} @@ -81,7 +80,7 @@ function CarouselItem({item, isFocused, onPress}) { {children} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js index 307dbe8e9ddb..d88eb81506ca 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js @@ -27,7 +27,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, o onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js index cb1190fa1fdd..8b29d8d5ba3d 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js @@ -35,7 +35,7 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUs onPress={onPress} disabled={loadComplete} style={[styles.flex1, styles.flexRow, styles.alignSelfStretch]} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={file.name || translate('attachmentView.unknownFilename')} > {children} diff --git a/src/components/Avatar.js b/src/components/Avatar.js index 4b8ddd45aa95..5e414486cc70 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -88,10 +88,7 @@ function Avatar(props) { const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(props.name) : props.fallbackIcon || Expensicons.FallbackAvatar; return ( - + {_.isFunction(props.source) || (imageError && _.isFunction(fallbackAvatar)) ? ( runOnUI(sliderOnPress)(e.nativeEvent.locationX)} accessibilityLabel="slider" - accessibilityRole={CONST.ACCESSIBILITY_ROLE.ADJUSTABLE} + role={CONST.ACCESSIBILITY_ROLE.ADJUSTABLE} > showActorDetails(props.report, props.shouldEnableDetailPageNavigation)} accessibilityLabel={title} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {shouldShowSubscriptAvatar ? ( ReportUtils.navigateToDetailsPage(props.report)} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} accessibilityLabel={title} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} > {headerView} diff --git a/src/components/AvatarWithImagePicker.js b/src/components/AvatarWithImagePicker.js index 871d967e23dc..87bd382e806b 100644 --- a/src/components/AvatarWithImagePicker.js +++ b/src/components/AvatarWithImagePicker.js @@ -264,7 +264,7 @@ class AvatarWithImagePicker extends React.Component { this.setState((prev) => ({isMenuVisible: !prev.isMenuVisible}))} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + role={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} accessibilityLabel={this.props.translate('avatarWithImagePicker.editImage')} disabled={this.state.isAvatarCropModalOpen} ref={this.anchorRef} diff --git a/src/components/Badge.js b/src/components/Badge.js index 0a6b72201655..49b330ae37b2 100644 --- a/src/components/Badge.js +++ b/src/components/Badge.js @@ -59,8 +59,9 @@ function Badge(props) { diff --git a/src/components/BigNumberPad.js b/src/components/BigNumberPad.js index ecbde3a5afe6..451b8fc3e0bf 100644 --- a/src/components/BigNumberPad.js +++ b/src/components/BigNumberPad.js @@ -16,14 +16,14 @@ const propTypes = { longPressHandlerStateChanged: PropTypes.func, /** Used to locate this view from native classes. */ - nativeID: PropTypes.string, + id: PropTypes.string, ...withLocalizePropTypes, }; const defaultProps = { longPressHandlerStateChanged: () => {}, - nativeID: 'numPadView', + id: 'numPadView', }; const padNumbers = [ @@ -59,7 +59,7 @@ function BigNumberPad(props) { return ( {_.map(padNumbers, (row, rowIndex) => ( { - if (!validateSubmitShortcut(this.props.isFocused, this.props.isDisabled, this.props.isLoading, e)) { - return; - } - this.props.onPress(); - }, - shortcutConfig.descriptionKey, - shortcutConfig.modifiers, - true, - this.props.allowBubble, - this.props.enterKeyEventListenerPriority, - false, - ); - } - - componentWillUnmount() { - // Cleanup event listeners - if (!this.unsubscribe) { - return; - } - this.unsubscribe(); - } - - renderContent() { - if (this.props.children) { - return this.props.children; +function Button({ + allowBubble, + text, + shouldShowRightIcon, + + icon, + iconRight, + iconFill, + iconStyles, + iconRightStyles, + + small, + large, + medium, + + isLoading, + isDisabled, + + onPress, + onLongPress, + onPressIn, + onPressOut, + onMouseDown, + + pressOnEnter, + enterKeyEventListenerPriority, + + style, + innerStyles, + textStyles, + + shouldUseDefaultHover, + success, + danger, + children, + + shouldRemoveRightBorderRadius, + shouldRemoveLeftBorderRadius, + shouldEnableHapticFeedback, + + id, + accessibilityLabel, + forwardedRef, +}) { + const isFocused = useIsFocused(); + + const keyboardShortcutCallback = useCallback( + (event) => { + if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) { + return; + } + onPress(); + }, + [isDisabled, isFocused, isLoading, onPress], + ); + + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, { + isActive: pressOnEnter, + shouldBubble: allowBubble, + priority: enterKeyEventListenerPriority, + shouldPreventDefault: false, + }); + + const renderContent = () => { + if (children) { + return children; } const textComponent = ( - {this.props.text} + {text} ); - if (this.props.icon || this.props.shouldShowRightIcon) { + if (icon || shouldShowRightIcon) { return ( - {this.props.icon && ( - + {icon && ( + )} {textComponent} - {this.props.shouldShowRightIcon && ( - + {shouldShowRightIcon && ( + )} @@ -257,87 +275,85 @@ class Button extends Component { } return textComponent; - } - - render() { - return ( - { - if (e && e.type === 'click') { - e.currentTarget.blur(); - } - - if (this.props.shouldEnableHapticFeedback) { - HapticFeedback.press(); - } - return this.props.onPress(e); - }} - onLongPress={(e) => { - if (this.props.shouldEnableHapticFeedback) { - HapticFeedback.longPress(); - } - this.props.onLongPress(e); - }} - onPressIn={this.props.onPressIn} - onPressOut={this.props.onPressOut} - onMouseDown={this.props.onMouseDown} - disabled={this.props.isLoading || this.props.isDisabled} - wrapperStyle={[ - this.props.isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {}, - styles.buttonContainer, - this.props.shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, - this.props.shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, - ...StyleUtils.parseStyleAsArray(this.props.style), - ]} - style={[ - styles.button, - this.props.small ? styles.buttonSmall : undefined, - this.props.medium ? styles.buttonMedium : undefined, - this.props.large ? styles.buttonLarge : undefined, - this.props.success ? styles.buttonSuccess : undefined, - this.props.danger ? styles.buttonDanger : undefined, - this.props.isDisabled && (this.props.success || this.props.danger) ? styles.buttonOpacityDisabled : undefined, - this.props.isDisabled && !this.props.danger && !this.props.success ? styles.buttonDisabled : undefined, - this.props.shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, - this.props.shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, - this.props.icon || this.props.shouldShowRightIcon ? styles.alignItemsStretch : undefined, - ...this.props.innerStyles, - ]} - hoverStyle={[ - this.props.shouldUseDefaultHover && !this.props.isDisabled ? styles.buttonDefaultHovered : undefined, - this.props.success && !this.props.isDisabled ? styles.buttonSuccessHovered : undefined, - this.props.danger && !this.props.isDisabled ? styles.buttonDangerHovered : undefined, - ]} - nativeID={this.props.nativeID} - accessibilityLabel={this.props.accessibilityLabel} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} - hoverDimmingValue={1} - > - {this.renderContent()} - {this.props.isLoading && ( - - )} - - ); - } + }; + + return ( + { + if (event && event.type === 'click') { + event.currentTarget.blur(); + } + + if (shouldEnableHapticFeedback) { + HapticFeedback.press(); + } + return onPress(event); + }} + onLongPress={(event) => { + if (shouldEnableHapticFeedback) { + HapticFeedback.longPress(); + } + onLongPress(event); + }} + onPressIn={onPressIn} + onPressOut={onPressOut} + onMouseDown={onMouseDown} + disabled={isLoading || isDisabled} + wrapperStyle={[ + isDisabled ? {...styles.cursorDisabled, ...styles.noSelect} : {}, + styles.buttonContainer, + shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, + shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, + ...StyleUtils.parseStyleAsArray(style), + ]} + style={[ + styles.button, + small ? styles.buttonSmall : undefined, + medium ? styles.buttonMedium : undefined, + large ? styles.buttonLarge : undefined, + success ? styles.buttonSuccess : undefined, + danger ? styles.buttonDanger : undefined, + isDisabled && (success || danger) ? styles.buttonOpacityDisabled : undefined, + isDisabled && !danger && !success ? styles.buttonDisabled : undefined, + shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, + shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, + icon || shouldShowRightIcon ? styles.alignItemsStretch : undefined, + ...innerStyles, + ]} + hoverStyle={[ + shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined, + success && !isDisabled ? styles.buttonSuccessHovered : undefined, + danger && !isDisabled ? styles.buttonDangerHovered : undefined, + ]} + id={id} + accessibilityLabel={accessibilityLabel} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + hoverDimmingValue={1} + > + {renderContent()} + {isLoading && ( + + )} + + ); } Button.propTypes = propTypes; Button.defaultProps = defaultProps; +Button.displayName = 'Button'; + +const ButtonWithRef = React.forwardRef((props, ref) => ( +