diff --git a/.github/actions/composite/buildAndroidAPK/action.yml b/.github/actions/composite/buildAndroidAPK/action.yml new file mode 100644 index 000000000000..819234df0bc3 --- /dev/null +++ b/.github/actions/composite/buildAndroidAPK/action.yml @@ -0,0 +1,29 @@ +name: Build an Android apk +description: Build an Android apk for an E2E test build and upload it as an artifact + +inputs: + ARTIFACT_NAME: + description: The name of the workflow artifact where the APK should be uploaded + required: true + +runs: + using: composite + steps: + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + with: + ruby-version: '2.7' + bundler-cache: true + + - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef + + - name: Build APK + run: npm run android-build-e2e + shell: bash + + - name: Upload APK + uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 + with: + name: ${{ inputs.ARTIFACT_NAME }} + path: android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk diff --git a/.github/scripts/validateActionsAndWorkflows.sh b/.github/scripts/validateActionsAndWorkflows.sh index 3785f7da3352..8d186ea51a44 100755 --- a/.github/scripts/validateActionsAndWorkflows.sh +++ b/.github/scripts/validateActionsAndWorkflows.sh @@ -43,7 +43,7 @@ for ((i=0; i < ${#WORKFLOWS[@]}; i++)); do WORKFLOW=${WORKFLOWS[$i]} # Skip linting e2e workflow due to bug here: https://github.com/SchemaStore/schemastore/issues/2579 - if [[ "$WORKFLOW" == './workflows/preDeploy.yml' ]]; then + if [[ "$WORKFLOW" == './workflows/e2ePerformanceTests.yml' ]]; then continue fi diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml new file mode 100644 index 000000000000..3d70d5d686c4 --- /dev/null +++ b/.github/workflows/e2ePerformanceTests.yml @@ -0,0 +1,173 @@ +name: E2E Performance Tests + +on: + workflow_call: + inputs: + PR_NUMBER: + description: A PR to run performance tests against. If already merged, the merge commit will be used. If not, the test merge commit will be used. + type: string + required: true + + workflow_dispatch: + inputs: + PR_NUMBER: + description: A PR to run performance tests against. If already merged, the merge commit will be used. If not, the test merge commit will be used. + type: string + required: true + +jobs: + prepare: + runs-on: ubuntu-latest + name: Prepare to run builds + outputs: + VERSION: ${{ steps.getMostRecentRelease.outputs.VERSION }} + DELTA_REF: ${{ steps.getMergeCommitSha.outputs.MERGE_COMMIT_SHA }} + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + + - name: Get most recent release version + id: getMostRecentRelease + run: echo "VERSION=$(gh release list --limit 1 | awk '{ print $1 }')" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Get pull request details + id: getPullRequestDetails + uses: Expensify/App/.github/actions/javascript/getPullRequestDetails@main + with: + GITHUB_TOKEN: ${{ github.token }} + PULL_REQUEST_NUMBER: ${{ inputs.PR_NUMBER }} + user: ${{ github.actor }} + + - name: Get merge commit sha for the pull request + id: getMergeCommitSha + run: echo "MERGE_COMMIT_SHA=${{ steps.getPullRequestDetails.outputs.MERGE_COMMIT_SHA }}" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ github.token }} + + buildBaseline: + runs-on: ubuntu-20.04-64core + needs: [prepare] + name: Build apk from latest release as a baseline + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + + - name: Check if there's an existing artifact for this baseline + id: checkForExistingArtifact + uses: xSAVIKx/artifact-exists-action@3c5206b1411c0d2fc0840f56b7140646933d9d6a + with: + name: baseline-apk-${{ needs.prepare.outputs.VERSION }} + + - name: Skip build if there's already an existing artifact for the baseline + if: ${{ fromJSON(steps.checkForExistingArtifact.outputs.exists) }} + run: echo 'APK for baseline ${{ needs.prepare.outputs.VERSION }} already exists, reusing existing build' + + - name: Checkout "Baseline" commit (last release) + if: ${{ !fromJSON(steps.checkForExistingArtifact.outputs.exists) }} + run: git checkout ${{ needs.prepare.outputs.VERSION }} + + - name: Build APK + if: ${{ !fromJSON(steps.checkForExistingArtifact.outputs.exists) }} + uses: Expensify/App/.github/actions/composite/buildAndroidAPK@main + with: + ARTIFACT_NAME: baseline-apk-${{ needs.prepare.outputs.VERSION }} + + buildDelta: + runs-on: ubuntu-20.04-64core + needs: [prepare] + name: Build apk from delta ref + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + + - name: Checkout "delta ref" + run: git checkout ${{ needs.prepare.outputs.DELTA_REF }} + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Build APK + uses: Expensify/App/.github/actions/composite/buildAndroidAPK@main + with: + ARTIFACT_NAME: delta-apk-${{ needs.prepare.outputs.DELTA_REF }} + + runTestsInAWS: + runs-on: ubuntu-20.04-64core + needs: [prepare, buildBaseline, buildDelta] + name: Run E2E tests in AWS device farm + steps: + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + ref: ${{ needs.prepare.outputs.DELTA_REF }} + + - name: Make zip directory for everything to send to AWS Device Farm + run: mkdir zip + + - name: Download baseline APK + uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b + with: + name: baseline-apk-${{ needs.prepare.outputs.VERSION }} + path: zip + + - name: Download delta APK + uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b + with: + name: delta-apk-${{ needs.prepare.outputs.DELTA_REF }} + path: zip + + - name: Copy e2e code into zip folder + run: cp -r tests/e2e zip + + - name: Zip everything in the zip directory up + run: zip -qr App.zip ./zip + + - name: Configure AWS Credentials + uses: Expensify/App/.github/actions/composite/configureAwsCredentials@main + with: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: us-west-2 + + - name: Schedule AWS Device Farm test run + uses: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b + with: + name: App E2E Performance Regression Tests + project_arn: ${{ secrets.AWS_PROJECT_ARN }} + device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} + app_file: zip/app-e2eRelease-baseline.apk + app_type: ANDROID_APP + test_type: APPIUM_NODE + test_package_file: App.zip + test_package_type: APPIUM_NODE_TEST_PACKAGE + test_spec_file: tests/e2e/TestSpec.yml + test_spec_type: APPIUM_NODE_TEST_SPEC + remote_src: false + file_artifacts: CustomerArtifacts.zip + cleanup: true + + - name: Unzip AWS Device Farm results + if: ${{ always() }} + run: unzip CustomerArtifacts.zip + + - name: Print AWS Device Farm run results + if: ${{ always() }} + run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/output.md" + + - name: Print AWS Device Farm verbose run results + if: ${{ always() && fromJSON(runner.debug) }} + run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/debug.log" + + - name: Check if test failed, if so post the results and add the DeployBlocker label + if: ${{ github.event_name == 'workflow_call' }} + run: | + if grep -q '🔴' ./Host_Machine_Files/\$WORKING_DIRECTORY/output.md; then + gh pr edit ${{ inputs.PR_NUMBER }} --add-label DeployBlockerCash + gh pr comment ${{ inputs.PR_NUMBER }} -F ./Host_Machine_Files/\$WORKING_DIRECTORY/output.md + gh pr comment ${{ inputs.PR_NUMBER }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." + else + echo '✅ no performance regression detected' + fi + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index af42e68bdabb..0e77dbe4704f 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -222,124 +222,10 @@ jobs: So it might take a while before you're paid for your work, but we typically post multiple new jobs every day, so there's plenty of opportunity. I hope you've had a positive experience contributing to this repo! :blush: - e2e-tests: - name: "Run e2e performance regression tests" - runs-on: ubuntu-20.04-64core - steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - fetch-depth: 0 - - - uses: Expensify/App/.github/actions/composite/setupNode@main - - - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 - with: - ruby-version: '2.7' - bundler-cache: true - - # Cache gradle to improve Android build time - - name: Gradle cache - uses: gradle/gradle-build-action@3fbe033aaae657f011f88f29be9e65ed26bd29ef - - - name: Make zip directory for everything to send to AWS Device Farm - run: mkdir zip - - - name: Checkout "Compare" commit - run: git checkout ${{ github.event.before }} - - - name: Install node packages - uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm ci - - - name: Build "Compare" APK - run: npm run android-build-e2e - - - name: Copy "Compare" APK - run: cp android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk zip/app-e2eRelease-compare.apk - - - name: Checkout "Baseline" commit (last release) - run: git checkout "$(gh release list --limit 1 | awk '{ print $1 }')" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install node packages - uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 - with: - timeout_minutes: 10 - max_attempts: 5 - command: npm ci - - - name: Build "Baseline" APK - run: npm run android-build-e2e - - - name: Copy "Baseline" APK - run: cp android/app/build/outputs/apk/e2eRelease/app-e2eRelease.apk zip/app-e2eRelease-baseline.apk - - - name: Checkout previous branch for source code to run on AWS Device farm - run: git checkout - - - - name: Copy e2e code into zip folder - run: cp -r tests/e2e zip - - - name: Zip everything in the zip directory up - run: zip -qr App.zip ./zip - - - name: Configure AWS Credentials - uses: Expensify/App/.github/actions/composite/configureAwsCredentials@main - with: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_REGION: us-west-2 - - - name: Schedule AWS Device Farm test run - uses: realm/aws-devicefarm/test-application@7b9a91236c456c97e28d384c9e476035d5ea686b - with: - name: App E2E Performance Regression Tests - project_arn: ${{ secrets.AWS_PROJECT_ARN }} - device_pool_arn: ${{ secrets.AWS_DEVICE_POOL_ARN }} - app_file: zip/app-e2eRelease-baseline.apk - app_type: ANDROID_APP - test_type: APPIUM_NODE - test_package_file: App.zip - test_package_type: APPIUM_NODE_TEST_PACKAGE - test_spec_file: tests/e2e/TestSpec.yml - test_spec_type: APPIUM_NODE_TEST_SPEC - remote_src: false - file_artifacts: Customer Artifacts.zip - cleanup: true - - - name: Unzip AWS Device Farm results - if: ${{ always() }} - run: unzip Customer\ Artifacts.zip - - - name: Print AWS Device Farm run results - if: ${{ always() }} - run: cat "./Host_Machine_Files/\$WORKING_DIRECTORY/debug.log" - - - name: Set output of AWS Device Farm into GitHub ENV - run: | - { echo 'OUTPUT<> "$GITHUB_ENV" - - - name: Get merged pull request - id: getMergedPullRequest - # TODO: Point back action actions-ecosystem after https://github.com/actions-ecosystem/action-get-merged-pull-request/pull/223 is merged - uses: roryabraham/action-get-merged-pull-request@7a7a194f6ff8f3eef58c822083695a97314ebec1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Leave output of AWS Device Farm as a PR comment - run: | - gh pr comment ${{ steps.getMergedPullRequest.outputs.number }} -F ./Host_Machine_Files/\$WORKING_DIRECTORY/output.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check if test failed, if so leave a deploy blocker label - if: ${{ contains(env.OUTPUT, '🔴') }} - run: | - gh pr edit ${{ steps.getMergedPullRequest.outputs.number }} --add-label 'DeployBlockerCash' - gh pr comment ${{ steps.getMergedPullRequest.outputs.number }} -b "@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + e2ePerformanceTests: + needs: [chooseDeployActions] + if: ${{ needs.chooseDeployActions.outputs.SHOULD_DEPLOY }} + uses: Expensify/App/.github/workflows/e2ePerformanceTests.yml@main + secrets: inherit + with: + PR_NUMBER: ${{ needs.chooseDeployActions.outputs.MERGED_PR }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 90919fdbf6a8..0f0cc258c8ea 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -156,8 +156,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001027704 - versionName "1.2.77-4" + versionCode 1001027800 + versionName "1.2.78-0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 71c3dc6c9af3..208076beeb71 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ @@ -15,7 +16,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:resizeableActivity="false" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:replace="android:supportsRtl"> -Once you have the extension installed, you can access it by going to https://github.com/Expensify/Expensify#k2 or clicking on the K2 tab at the top of a repo (between pull requests and GH actions). You'll have to create a Personal Access Token in GitHub ([here](https://github.com/settings/tokens)) and enter that in the K2 dashboard the first time you open it, so that your K2 extension can automatically pull data about your GitHub account. +Once you have the extension installed, you can access it by going to https://github.com/Expensify/App#k2 or clicking on the K2 tab at the top of a repo (between pull requests and GH actions). You'll have to create a Personal Access Token in GitHub ([here](https://github.com/settings/tokens)) and enter that in the K2 dashboard the first time you open it, so that your K2 extension can automatically pull data about your GitHub account. ### Pull Requests for review diff --git a/contributingGuides/TestRail_Navigation_C+.md b/contributingGuides/TestRail_Navigation_C+.md index 88c4a4ed3bc3..6496664993b2 100644 --- a/contributingGuides/TestRail_Navigation_C+.md +++ b/contributingGuides/TestRail_Navigation_C+.md @@ -20,14 +20,10 @@ As a C+ member, you will have view-only access to Expensify's TestRail account i - Once determined, the C+ will post a comment in the original GH issue, mentioning which scenario the bug belongs under, why they think it belongs there, and if the new / updated test steps can fall under a current test case or if a new test case needs to be created. Please provide your reasoning for your decision in the comment and tag the BZ member to gut-check. - If the BZ member agrees with the C+'s recommendation, we can move forward. If not, a discussion will be held on where they think it might fit better and why. - There's a chance we will agree to not update/create a test case for the bug in question, depending on the bug. -- Once we know where the test will live, the C+ will tag the Contributor to propose test steps. - - If we're updating a current test case, the C+ will post what the current steps are in the GH for the Contributor to propose their test steps in relation to it. - - If we're creating a new test case, the C+ will note it for the Contributor. -- Once the Contributor has provided proposed test steps, the C+ will review to ensure: +- Once we know where the test will live, the C+ will then propose the appropriate test steps to either add to an existing case or for a new test case. +- Once the C+ has provided proposed test steps, the BZ will review to ensure: - The language style matches the language style in TestRail (e.g. action items use the term `Verify`) - The steps are clear, logical, concise, and void of any assumed knowledge - (For updating a current test case) the steps are logically placed in the test case - - If changes are needed, the C+ and Contributor will discuss what changes should be done in order to make them appropriate -- After confirming the above, the C+ will tag the BZ member and comment that the steps are ready to be reviewed + - If changes are needed, the BZ member and C+ will discuss what changes should be done in order to make them appropriate - The BZ member will then create a GH for Applause to update the TestRail steps, link it in the original bug GH, and move forward with payment. - diff --git a/desktop/main.js b/desktop/main.js index 9381ff0e2dfe..484398f8c363 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -246,7 +246,7 @@ const mainWindow = (() => { app.on('open-url', (event, url) => { event.preventDefault(); const urlObject = new URL(url); - deeplinkUrl = `${APP_DOMAIN}${urlObject.pathname}`; + deeplinkUrl = `${APP_DOMAIN}${urlObject.pathname}${urlObject.search}${urlObject.hash}`; if (browserWindow) { browserWindow.loadURL(deeplinkUrl); diff --git a/docs/_data/routes.yml b/docs/_data/routes.yml index c1dfea54c03d..0eb26895e9ec 100644 --- a/docs/_data/routes.yml +++ b/docs/_data/routes.yml @@ -38,6 +38,8 @@ hubs: title: Expensify Playbook for US-Based VC-Backed Startups - href: Expensify-Playbook-for-US-Based-Bootstrapped-Startups title: Expensify Playbook for US-Based Bootstrapped Startups + - href: Expensify-Playbook-for-US-Based-Small-Businesses + title: Expensify Playbook for US-Based Small Businesses - href: other title: Other diff --git a/docs/_includes/footer.html b/docs/_includes/footer.html index bbbf9fb3117d..166c1fa569d2 100644 --- a/docs/_includes/footer.html +++ b/docs/_includes/footer.html @@ -110,7 +110,7 @@

Get Started

- + @@ -118,4 +118,4 @@

Get Started

Expensify - \ No newline at end of file + diff --git a/SmallbusinessPlaybook b/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md similarity index 99% rename from SmallbusinessPlaybook rename to docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md index c599a466af37..17053f5b9680 100644 --- a/SmallbusinessPlaybook +++ b/docs/articles/playbooks/Expensify-Playbook-for-US-Based-Small-Businesses.md @@ -1,5 +1,5 @@ --- -title: Expensify Playbook for US Small Businesses +title: Expensify Playbook for US Based Small Businesses description: Best practices for how to deploy Expensify for your business --- This guide provides practical tips and recommendations for small businesses with under 100 employees to effectively use Expensify to improve spend visibility, facilitate employee reimbursements, and reduce the risk of fraudulent expenses. diff --git a/docs/assets/js/main.js b/docs/assets/js/main.js index c335047466e1..38f495c753cf 100644 --- a/docs/assets/js/main.js +++ b/docs/assets/js/main.js @@ -58,7 +58,14 @@ function navigateBack() { setTimeout(toggleHeaderMenu, 250); } +function injectFooterCopywrite() { + const footer = document.getElementById('footer-copywrite-date'); + footer.innerHTML = `©2008-${new Date().getFullYear()} Expensify, Inc.`; +} + window.addEventListener('DOMContentLoaded', () => { + injectFooterCopywrite(); + if (window.tocbot) { window.tocbot.init({ // Where to render the table of contents. diff --git a/ios/ExpensifyCash-Bridging-Header.h b/ios/ExpensifyCash-Bridging-Header.h deleted file mode 100644 index 2420459f9630..000000000000 --- a/ios/ExpensifyCash-Bridging-Header.h +++ /dev/null @@ -1,2 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. \ No newline at end of file diff --git a/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d68..000000000000 --- a/ios/ExpensifyCash.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index b3dab9cee754..ee77a2595272 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 00E356F31AD99517003FC87E /* ExpensifyCashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ExpensifyCashTests.m */; }; 0CDA8E34287DD650004ECBEC /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */; }; 0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0CDA8E33287DD650004ECBEC /* AppDelegate.mm */; }; 0CDA8E37287DD6A0004ECBEC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CDA8E36287DD6A0004ECBEC /* Images.xcassets */; }; @@ -50,7 +49,6 @@ 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* NewExpensifyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewExpensifyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 00E356F21AD99517003FC87E /* ExpensifyCashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExpensifyCashTests.m; sourceTree = ""; }; 01B491F27448B52C69E394BB /* Pods-NewExpensify-NewExpensifyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.release.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.release.xcconfig"; sourceTree = ""; }; 01CEE12C42AABB00D041967F /* Pods-NewExpensify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.release.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.release.xcconfig"; sourceTree = ""; }; 02BB59C8C4E7D63E4192F2FA /* Pods-NewExpensify-NewExpensifyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify-NewExpensifyTests.debug.xcconfig"; path = "Target Support Files/Pods-NewExpensify-NewExpensifyTests/Pods-NewExpensify-NewExpensifyTests.debug.xcconfig"; sourceTree = ""; }; @@ -111,7 +109,6 @@ 00E356EF1AD99517003FC87E /* NewExpensifyTests */ = { isa = PBXGroup; children = ( - 00E356F21AD99517003FC87E /* ExpensifyCashTests.m */, 00E356F01AD99517003FC87E /* Supporting Files */, ); path = NewExpensifyTests; @@ -558,7 +555,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00E356F31AD99517003FC87E /* ExpensifyCashTests.m in Sources */, 0F5E5351263B73FD004CA14F /* EnvironmentChecker.m in Sources */, 0CDA8E35287DD650004ECBEC /* AppDelegate.mm in Sources */, 7041848626A8E47D00E09F4D /* RCTStartupTimer.m in Sources */, @@ -663,7 +659,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = "New Expensify"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; - SWIFT_OBJC_BRIDGING_HEADER = "ExpensifyCash-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -695,7 +690,6 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = "New Expensify"; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; - SWIFT_OBJC_BRIDGING_HEADER = "ExpensifyCash-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4a79074f3e22..d427cc8a061c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.77 + 1.2.78 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.77.4 + 1.2.78.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 680745dcee4e..99534496e50d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.77 + 1.2.78 CFBundleSignature ???? CFBundleVersion - 1.2.77.4 + 1.2.78.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ed9cdc3f6850..f494e566c73e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -177,29 +177,29 @@ PODS: - GoogleUtilities/Network (~> 7.4) - "GoogleUtilities/NSData+zlib (~> 7.4)" - nanopb (~> 2.30908.0) - - GoogleDataTransport (9.2.0): + - GoogleDataTransport (9.2.1): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.10.0): + - GoogleUtilities/AppDelegateSwizzler (7.11.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.10.0): + - GoogleUtilities/Environment (7.11.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.10.0) - - GoogleUtilities/Logger (7.10.0): + - GoogleUtilities/ISASwizzler (7.11.0) + - GoogleUtilities/Logger (7.11.0): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.10.0): + - GoogleUtilities/MethodSwizzler (7.11.0): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.10.0): + - GoogleUtilities/Network (7.11.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.10.0)" - - GoogleUtilities/Reachability (7.10.0): + - "GoogleUtilities/NSData+zlib (7.11.0)" + - GoogleUtilities/Reachability (7.11.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.10.0): + - GoogleUtilities/UserDefaults (7.11.0): - GoogleUtilities/Logger - hermes-engine (0.70.4) - libevent (2.1.12) @@ -213,7 +213,7 @@ PODS: - libwebp/demux - libwebp/webp (1.2.4) - lottie-ios (3.4.4) - - lottie-react-native (5.1.4): + - lottie-react-native (5.1.5): - lottie-ios (~> 3.4.0) - React-Core - nanopb (2.30908.0): @@ -221,9 +221,9 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - - Onfido (27.0.0) - - onfido-react-native-sdk (7.0.1): - - Onfido (= 27.0.0) + - Onfido (27.4.0) + - onfido-react-native-sdk (7.4.0): + - Onfido (= 27.4.0) - React - OpenSSL-Universal (1.1.1100) - Permission-Camera (3.6.1): @@ -235,7 +235,7 @@ PODS: - Permission-LocationWhenInUse (3.6.1): - RNPermissions - Plaid (2.5.1) - - PromisesObjC (2.1.1) + - PromisesObjC (2.2.0) - RCT-Folly (2021.07.22.00): - boost - DoubleConversion @@ -969,23 +969,23 @@ SPEC CHECKSUMS: fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91 - GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f - GoogleUtilities: bad72cb363809015b1f7f19beb1f1cd23c589f95 + GoogleDataTransport: ea169759df570f4e37bdee1623ec32a7e64e67c4 + GoogleUtilities: c2bdc4cf2ce786c4d2e6b3bcfd599a25ca78f06f hermes-engine: 3623325e0d0676a45fbc544d72c57dd79fce7446 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef lottie-ios: 8f97d3271e155c2d688875c29cd3c74908aef5f8 - lottie-react-native: b702fab740cdb952a8e2354713d3beda63ff97b0 + lottie-react-native: 3e722c63015fdb9c27638b0a77969fc412067c18 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - Onfido: bdbc3ed45598aa106ab2ea021d94e2e28c6b5be3 - onfido-react-native-sdk: 5856e76fbfc0eb7b70b0f76fa1059830932a5c88 + Onfido: e36f284b865adcf99d9c905590a64ac09d4a576b + onfido-react-native-sdk: 4ecde1a97435dcff9f00a878e3f8d1eb14fabbdc OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c Permission-Camera: bf6791b17c7f614b6826019fcfdcc286d3a107f6 Permission-LocationAccuracy: 76df17de5c6b8bc2eee34e61ee92cdd7a864c73d Permission-LocationAlways: 8d99b025c9f73c696e0cdb367e42525f2e9a26f2 Permission-LocationWhenInUse: 3ba99e45c852763f730eabecec2870c2382b7bd4 Plaid: 6beadc0828cfd5396c5905931b9503493bbc139a - PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb + PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: 329ead02b8edd20fb186d17745a9cadd5ce2922d RCTTypeSafety: 698418021f8b47d82c058f3115c0026d1874a3ef diff --git a/package-lock.json b/package-lock.json index 58653dd9cdd8..9c5704b7b568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.77-4", + "version": "1.2.78-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.77-4", + "version": "1.2.78-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -16,8 +16,8 @@ "@formatjs/intl-numberformat": "^6.2.5", "@formatjs/intl-pluralrules": "^4.0.13", "@gorhom/portal": "^1.0.14", - "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "@onfido/react-native-sdk": "7.0.1", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", + "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-community/cameraroll": "git+https://github.com/react-native-cameraroll/react-native-cameraroll.git#3f0aed96db68e134f199171c7b06c1b4d6cb382b", "@react-native-community/clipboard": "^1.5.1", @@ -37,18 +37,18 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#69e32de34d28098c7316a66e28061d8df50f86c5", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#fff304449214480065bf550e562ad39e681f6f95", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", "jest-when": "^3.5.2", "localforage": "^1.10.0", "lodash": "4.17.21", - "lottie-react-native": "^5.1.4", + "lottie-react-native": "^5.1.5", "metro-config": "^0.71.3", "moment": "^2.29.4", "moment-timezone": "^0.5.31", - "onfido-sdk-ui": "10.3.0", + "onfido-sdk-ui": "10.4.0", "process": "^0.11.10", "prop-types": "^15.7.2", "pusher-js": "^7.0.6", @@ -5024,8 +5024,8 @@ }, "node_modules/@oguzhnatly/react-native-image-manipulator": { "version": "1.0.5", - "resolved": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", + "resolved": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", + "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", "license": "MIT" }, "node_modules/@onfido/active-video-capture": { @@ -5088,9 +5088,9 @@ } }, "node_modules/@onfido/react-native-sdk": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-7.0.1.tgz", - "integrity": "sha512-nhjByw/YyTACvkDWX2QtCzYmqkrDtSBJxYYgJjPuKvPRVIJhrny3bIm0DzAi1hWyIM2ZsKW/MSQxerGhR9FQaw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-7.4.0.tgz", + "integrity": "sha512-qeeaXLxVXz+J0lrqMwQGP52fXhCnTmEAC5K8jZe8YTqst2s1FZZGKkd1bxTloHG5hBBTa39SwWVUKmgPUm+Ssw==", "peerDependencies": { "react": ">=17.0.0", "react-native": ">=0.68.2 <1.0.x" @@ -25230,8 +25230,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#69e32de34d28098c7316a66e28061d8df50f86c5", - "integrity": "sha512-dW+Lt6EO2YpsgX7ySRa9obqxI2/SO9LZlWOq6gG9MqMX2Wj6qAStgwIOWKLB6GJekmIj5nri+wVisrV5NV7hSw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#fff304449214480065bf550e562ad39e681f6f95", + "integrity": "sha512-18tm0Y8R4vwUJNPmeA5KIv0BAUuCti6YEzwZZ3ty+0/yPFkISGSMkIyPJ2/4CVE2FXIJkIuHaDpUmtp0MUaW7Q==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -32733,9 +32733,9 @@ "peer": true }, "node_modules/lottie-react-native": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-5.1.4.tgz", - "integrity": "sha512-Lu6mSG92Wck+vXEX6gfj/9ciqqoz0tJQZqgX0SumGvX/oZu4MbKO/oLApyHdy2V9Rb7qvwF9whOtitADxTswPA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-5.1.5.tgz", + "integrity": "sha512-xl6uEo50joQeIqso5SvPKt1uGBqMhgNgs+36S4725Nfigf4zAY23/I9QEEkJF+1BHq7wKCeYha2KafLTm20gqA==", "dependencies": { "invariant": "^2.2.2", "react-native-safe-modules": "^1.0.3" @@ -35522,9 +35522,9 @@ } }, "node_modules/onfido-sdk-ui": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-10.3.0.tgz", - "integrity": "sha512-53Yr9s9fb3heMPserJiJY8+Yf5XvRSBVXhswylhlosrZivr5ygEeweteSmdb/ICm2qpqYA8MAVNgzRKnFaImIA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-10.4.0.tgz", + "integrity": "sha512-N/GwvtRUxhV5fH9lkoz8FTb8XvwAllCdgQB/YxomJ3yaFgU8OK3XmPscQ3b0edhaRohXDKyBvsbO23lEcSSq8Q==", "dependencies": { "@onfido/active-video-capture": "^0.22.1", "@onfido/opencv": "^1.0.0", @@ -48556,9 +48556,9 @@ } }, "@oguzhnatly/react-native-image-manipulator": { - "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", - "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52" + "version": "git+ssh://git@github.com/Expensify/react-native-image-manipulator.git#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", + "integrity": "sha512-PvrSoCq5PS1MA5ZWUpB0khfzH6sM8SI6YiVl4i2SItPr7IeRxiWfI4n45VhBCCElc1z5GhAwTZOBaIzXTX7/og==", + "from": "@oguzhnatly/react-native-image-manipulator@github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050" }, "@onfido/active-video-capture": { "version": "0.22.1", @@ -48611,9 +48611,9 @@ } }, "@onfido/react-native-sdk": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-7.0.1.tgz", - "integrity": "sha512-nhjByw/YyTACvkDWX2QtCzYmqkrDtSBJxYYgJjPuKvPRVIJhrny3bIm0DzAi1hWyIM2ZsKW/MSQxerGhR9FQaw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-7.4.0.tgz", + "integrity": "sha512-qeeaXLxVXz+J0lrqMwQGP52fXhCnTmEAC5K8jZe8YTqst2s1FZZGKkd1bxTloHG5hBBTa39SwWVUKmgPUm+Ssw==", "requires": {} }, "@pmmmwh/react-refresh-webpack-plugin": { @@ -64003,9 +64003,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#69e32de34d28098c7316a66e28061d8df50f86c5", - "integrity": "sha512-dW+Lt6EO2YpsgX7ySRa9obqxI2/SO9LZlWOq6gG9MqMX2Wj6qAStgwIOWKLB6GJekmIj5nri+wVisrV5NV7hSw==", - "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#69e32de34d28098c7316a66e28061d8df50f86c5", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#fff304449214480065bf550e562ad39e681f6f95", + "integrity": "sha512-18tm0Y8R4vwUJNPmeA5KIv0BAUuCti6YEzwZZ3ty+0/yPFkISGSMkIyPJ2/4CVE2FXIJkIuHaDpUmtp0MUaW7Q==", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#fff304449214480065bf550e562ad39e681f6f95", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -69657,9 +69657,9 @@ "peer": true }, "lottie-react-native": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-5.1.4.tgz", - "integrity": "sha512-Lu6mSG92Wck+vXEX6gfj/9ciqqoz0tJQZqgX0SumGvX/oZu4MbKO/oLApyHdy2V9Rb7qvwF9whOtitADxTswPA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/lottie-react-native/-/lottie-react-native-5.1.5.tgz", + "integrity": "sha512-xl6uEo50joQeIqso5SvPKt1uGBqMhgNgs+36S4725Nfigf4zAY23/I9QEEkJF+1BHq7wKCeYha2KafLTm20gqA==", "requires": { "invariant": "^2.2.2", "react-native-safe-modules": "^1.0.3" @@ -71879,9 +71879,9 @@ } }, "onfido-sdk-ui": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-10.3.0.tgz", - "integrity": "sha512-53Yr9s9fb3heMPserJiJY8+Yf5XvRSBVXhswylhlosrZivr5ygEeweteSmdb/ICm2qpqYA8MAVNgzRKnFaImIA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/onfido-sdk-ui/-/onfido-sdk-ui-10.4.0.tgz", + "integrity": "sha512-N/GwvtRUxhV5fH9lkoz8FTb8XvwAllCdgQB/YxomJ3yaFgU8OK3XmPscQ3b0edhaRohXDKyBvsbO23lEcSSq8Q==", "requires": { "@onfido/active-video-capture": "^0.22.1", "@onfido/opencv": "^1.0.0", diff --git a/package.json b/package.json index cd7c578edf64..d5ed53feb4b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.77-4", + "version": "1.2.78-0", "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.", @@ -47,8 +47,8 @@ "@formatjs/intl-numberformat": "^6.2.5", "@formatjs/intl-pluralrules": "^4.0.13", "@gorhom/portal": "^1.0.14", - "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#5cdae3d4455b03a04c57f50be3863e2fe6c92c52", - "@onfido/react-native-sdk": "7.0.1", + "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", + "@onfido/react-native-sdk": "7.4.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-community/cameraroll": "git+https://github.com/react-native-cameraroll/react-native-cameraroll.git#3f0aed96db68e134f199171c7b06c1b4d6cb382b", "@react-native-community/clipboard": "^1.5.1", @@ -68,18 +68,18 @@ "babel-polyfill": "^6.26.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#69e32de34d28098c7316a66e28061d8df50f86c5", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#fff304449214480065bf550e562ad39e681f6f95", "fbjs": "^3.0.2", "html-entities": "^1.3.1", "htmlparser2": "^7.2.0", "jest-when": "^3.5.2", "localforage": "^1.10.0", "lodash": "4.17.21", - "lottie-react-native": "^5.1.4", + "lottie-react-native": "^5.1.5", "metro-config": "^0.71.3", "moment": "^2.29.4", "moment-timezone": "^0.5.31", - "onfido-sdk-ui": "10.3.0", + "onfido-sdk-ui": "10.4.0", "process": "^0.11.10", "prop-types": "^15.7.2", "pusher-js": "^7.0.6", diff --git a/scripts/set-pusher-suffix.sh b/scripts/set-pusher-suffix.sh index e1620b149f7b..eaa51f1425c0 100755 --- a/scripts/set-pusher-suffix.sh +++ b/scripts/set-pusher-suffix.sh @@ -4,9 +4,13 @@ # config file to be parsed for the suffix (relative to current project root) CONFIG_FILE="../Web-Expensify/_config.local.php" -if [ -f ".env" ]; then - # Export vars from the .env file to access the $EXPENSIFY_URL - export "$(grep -v '^#' .env | xargs)" +if [ -f '.env' ]; then + while read -r line; do + if [[ "$line" == \#* ]]; then + continue + fi + export "${line?}" + done < .env fi # use the suffix only when the config file can be found diff --git a/src/CONST.js b/src/CONST.js index accd263483f4..30f9e24ae3e7 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -402,6 +402,7 @@ const CONST = { SHOW_LOADING_SPINNER_DEBOUNCE_TIME: 250, TOOLTIP_SENSE: 1000, TRIE_INITIALIZATION: 'trie_initialization', + COMMENT_LENGTH_DEBOUNCE_TIME: 500, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 6a0167fe937a..6eeef6d3fa1b 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -14,6 +14,7 @@ import * as Browser from '../../libs/Browser'; import Clipboard from '../../libs/Clipboard'; import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import compose from '../../libs/compose'; +import styles from '../../styles/styles'; const propTypes = { /** Maximum number of lines in the text input */ @@ -357,7 +358,13 @@ class Composer extends React.Component { onChange={this.shouldCallUpdateNumberOfLines} onSelectionChange={this.onSelectionChange} numberOfLines={this.state.numberOfLines} - style={propStyles} + style={[ + propStyles, + + // We are hiding the scrollbar to prevent it from reducing the text input width, + // so we can get the correct scroll height while calculating the number of lines. + this.state.numberOfLines < this.props.maxLines ? styles.overflowHidden : {}, + ]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsWithoutStyles} disabled={this.props.isDisabled} diff --git a/src/components/DeeplinkWrapper/index.website.js b/src/components/DeeplinkWrapper/index.website.js index cfcda9660ca8..5179ffa99da0 100644 --- a/src/components/DeeplinkWrapper/index.website.js +++ b/src/components/DeeplinkWrapper/index.website.js @@ -74,7 +74,7 @@ class DeeplinkWrapper extends PureComponent { openRouteInDesktopApp() { const expensifyUrl = new URL(CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL); - const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}${window.location.pathname}`; + const expensifyDeeplinkUrl = `${CONST.DEEPLINK_BASE_URL}${expensifyUrl.host}${window.location.pathname}${window.location.search}${window.location.hash}`; // This check is necessary for Safari, otherwise, if the user // does NOT have the Expensify desktop app installed, it's gonna diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js index 33f557c66d99..4ef6a5027e73 100644 --- a/src/components/ExceededCommentLength.js +++ b/src/components/ExceededCommentLength.js @@ -1,27 +1,62 @@ -import React from 'react'; +import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {debounce} from 'lodash'; import CONST from '../CONST'; +import * as ReportUtils from '../libs/ReportUtils'; import Text from './Text'; import styles from '../styles/styles'; const propTypes = { - /** The current length of the comment */ - commentLength: PropTypes.number.isRequired, + /** Text Comment */ + comment: PropTypes.string.isRequired, + + /** Update UI on parent when comment length is exceeded */ + onExceededMaxCommentLength: PropTypes.func.isRequired, }; -const ExceededCommentLength = (props) => { - if (props.commentLength <= CONST.MAX_COMMENT_LENGTH) { - return null; +class ExceededCommentLength extends PureComponent { + constructor(props) { + super(props); + + this.state = { + commentLength: 0, + }; + + // By debouncing, we defer the calculation until there is a break in typing + this.updateCommentLength = debounce(this.updateCommentLength.bind(this), CONST.TIMING.COMMENT_LENGTH_DEBOUNCE_TIME); } - return ( - - {`${props.commentLength}/${CONST.MAX_COMMENT_LENGTH}`} - - ); -}; + componentDidMount() { + this.updateCommentLength(); + } + + componentDidUpdate(prevProps) { + if (prevProps.comment === this.props.comment) { + return; + } + + this.updateCommentLength(); + } + + updateCommentLength() { + const commentLength = ReportUtils.getCommentLength(this.props.comment); + this.setState({commentLength}); + this.props.onExceededMaxCommentLength(commentLength > CONST.MAX_COMMENT_LENGTH); + } + + render() { + if (this.state.commentLength <= CONST.MAX_COMMENT_LENGTH) { + return null; + } + + return ( + + {`${this.state.commentLength}/${CONST.MAX_COMMENT_LENGTH}`} + + ); + } +} ExceededCommentLength.propTypes = propTypes; -ExceededCommentLength.displayName = 'ExceededCommentLength'; export default ExceededCommentLength; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js index e52aa318f6dd..d9ec687d59d5 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer/BasePreRenderer.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import htmlRendererPropTypes from '../htmlRendererPropTypes'; import withLocalize from '../../../withLocalize'; import {ShowContextMenuContext, showContextMenuForReport} from '../../../ShowContextMenuContext'; +import styles from '../../../../styles/styles'; const propTypes = { /** Press in handler for the code block */ @@ -30,6 +31,7 @@ const BasePreRenderer = forwardRef((props, ref) => { {({ diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js index 53baab7d64fa..ea012dd32417 100755 --- a/src/components/IOUConfirmationList.js +++ b/src/components/IOUConfirmationList.js @@ -44,7 +44,7 @@ const propTypes = { login: PropTypes.string.isRequired, alternateText: PropTypes.string, hasDraftComment: PropTypes.bool, - icons: PropTypes.arrayOf(PropTypes.string), + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), searchText: PropTypes.string, text: PropTypes.string, keyForList: PropTypes.string, diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js index 5be074168250..e2ad0be35137 100644 --- a/src/components/LocalePicker.js +++ b/src/components/LocalePicker.js @@ -7,7 +7,6 @@ import * as App from '../libs/actions/App'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; -import * as Localize from '../libs/Localize'; import Picker from './Picker'; import styles from '../styles/styles'; @@ -26,33 +25,31 @@ const defaultProps = { size: 'normal', }; -const localesToLanguages = { - default: { - value: 'en', - label: Localize.translate('en', 'languagePage.languages.en.label'), - }, - es: { - value: 'es', - label: Localize.translate('es', 'languagePage.languages.es.label'), - }, -}; - -const LocalePicker = props => ( - { - if (locale === props.preferredLocale) { - return; - } +const LocalePicker = (props) => { + const localesToLanguages = _.map( + props.translate('languagePage.languages'), + (language, key) => ({ + value: key, + label: language.label, + }), + ); + return ( + { + if (locale === props.preferredLocale) { + return; + } - App.setLocale(locale); - }} - items={_.values(localesToLanguages)} - size={props.size} - value={props.preferredLocale} - containerStyles={props.size === 'small' ? [styles.pickerContainerSmall] : []} - /> -); + App.setLocale(locale); + }} + items={localesToLanguages} + size={props.size} + value={props.preferredLocale} + containerStyles={props.size === 'small' ? [styles.pickerContainerSmall] : []} + /> + ); +}; LocalePicker.defaultProps = defaultProps; LocalePicker.propTypes = propTypes; diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.js index c3e58bc674b0..5901fa04a5cc 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.js @@ -63,16 +63,12 @@ class Onfido extends React.Component { options: { useLiveDocumentCapture: true, forceCrossDevice: true, - showCountrySelection: false, + hideCountrySelection: true, + country: 'USA', + uploadFallback: false, documentTypes: { driving_licence: { - country: null, - }, - national_identity_card: { - country: null, - }, - residence_permit: { - country: null, + country: 'USA', }, passport: true, }, diff --git a/src/components/ReportActionItem/IOUAction.js b/src/components/ReportActionItem/IOUAction.js index 5c6d207e8edd..a7fe3f17102a 100644 --- a/src/components/ReportActionItem/IOUAction.js +++ b/src/components/ReportActionItem/IOUAction.js @@ -96,6 +96,7 @@ const IOUAction = (props) => { shouldAllowViewDetails={Boolean(props.action.originalMessage.IOUReportID)} onViewDetailsPressed={launchDetailsModal} checkIfContextMenuActive={props.checkIfContextMenuActive} + isHovered={props.isHovered} /> {shouldShowIOUPreview && ( {}, checkIfContextMenuActive: () => {}, }; @@ -80,7 +86,7 @@ const IOUQuote = props => ( {Str.htmlDecode(fragment.text.substring(fragment.text.indexOf(' ')))} - + ))} diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 31ad10981cec..e3772b055bf5 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; import _ from 'underscore'; import lodashMerge from 'lodash/merge'; +import lodashFindLast from 'lodash/findLast'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Onyx from 'react-native-onyx'; import moment from 'moment'; @@ -156,18 +157,15 @@ function getLastVisibleMessageText(reportID, actionsToMerge = {}) { } /** - * This method returns the report actions that are ready for display in the ReportActionsView. - * The report actions need to be sorted by created timestamp first, and reportActionID second - * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). - * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. + * A helper method to filter out report actions keyed by sequenceNumbers. * * @param {Object} reportActions * @returns {Array} */ -function getSortedReportActionsForDisplay(reportActions) { +function filterOutDeprecatedReportActions(reportActions) { // HACK ALERT: We're temporarily filtering out any reportActions keyed by sequenceNumber // to prevent bugs during the migration from sequenceNumber -> reportActionID - const filteredReportActions = _.filter(reportActions, (reportAction, key) => { + return _.filter(reportActions, (reportAction, key) => { if (!reportAction) { return false; } @@ -179,7 +177,19 @@ function getSortedReportActionsForDisplay(reportActions) { return true; }); +} +/** + * This method returns the report actions that are ready for display in the ReportActionsView. + * The report actions need to be sorted by created timestamp first, and reportActionID second + * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). + * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. + * + * @param {Object} reportActions + * @returns {Array} + */ +function getSortedReportActionsForDisplay(reportActions) { + const filteredReportActions = filterOutDeprecatedReportActions(reportActions); const sortedReportActions = getSortedReportActions(filteredReportActions, true); return _.filter(sortedReportActions, (reportAction) => { // Filter out any unsupported reportAction types @@ -199,6 +209,19 @@ function getSortedReportActionsForDisplay(reportActions) { }); } +/** + * In some cases, there can be multiple closed report actions in a chat report. + * This method returns the last closed report action so we can always show the correct archived report reason. + * + * @param {Object} reportActions + * @returns {Object} + */ +function getLastClosedReportAction(reportActions) { + const filteredReportActions = filterOutDeprecatedReportActions(reportActions); + const sortedReportActions = getSortedReportActions(filteredReportActions); + return lodashFindLast(sortedReportActions, action => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED); +} + export { getSortedReportActions, getLastVisibleAction, @@ -207,4 +230,5 @@ export { isDeletedAction, isConsecutiveActionMadeByPreviousActor, getSortedReportActionsForDisplay, + getLastClosedReportAction, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4c03e35b642c..a911ee26b005 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -129,6 +129,7 @@ function canEditReportAction(reportAction) { return reportAction.actorEmail === sessionEmail && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) + && !ReportActionsUtils.isDeletedAction(reportAction) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } @@ -281,10 +282,17 @@ function isArchivedRoom(report) { * @param {Object} report * @param {String} report.policyID * @param {String} report.oldPolicyName + * @param {String} report.policyName * @param {Object} policies must have Onyxkey prefix (i.e 'policy_') for keys * @returns {String} */ function getPolicyName(report, policies) { + // Public rooms send back the policy name with the reportSummary, + // since they can also be accessed by people who aren't in the workspace + if (report.policyName) { + return report.policyName; + } + if (_.isEmpty(policies)) { return Localize.translateLocal('workspace.common.unavailable'); } @@ -774,6 +782,15 @@ function hasReportNameError(report) { return !_.isEmpty(lodashGet(report, 'errorFields.reportName', {})); } +/** + * @param {String} text + * @returns {String} + */ +function getParsedComment(text) { + const parser = new ExpensiMark(); + return text.length < CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : text; +} + /** * @param {String} [text] * @param {File} [file] @@ -783,7 +800,7 @@ function buildOptimisticAddCommentReportAction(text, file) { // For comments shorter than 10k chars, convert the comment from MD into HTML because that's how it is stored in the database // For longer comments, skip parsing and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! const parser = new ExpensiMark(); - const commentText = text.length < CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : text; + const commentText = getParsedComment(text); const isAttachment = _.isEmpty(text) && file !== undefined; const attachmentInfo = isAttachment ? file : {}; const htmlForNewComment = isAttachment ? 'Uploading Attachment...' : commentText; @@ -1380,7 +1397,7 @@ function getChatByParticipants(newParticipantList) { } // Only return the room if it has all the participants and is not a policy room - return !isUserCreatedPolicyRoom(report) && _.isEqual(newParticipantList, report.participants.sort()); + return !isUserCreatedPolicyRoom(report) && _.isEqual(newParticipantList, _.sortBy(report.participants)); }); } @@ -1424,13 +1441,13 @@ function getNewMarkerReportActionID(report, sortedAndFilteredReportActions) { } /** - * Replace code points > 127 with C escape sequences, and return the resulting string's overall length - * Used for compatibility with the backend auth validator for AddComment + * Performs the markdown conversion, and replaces code points > 127 with C escape sequences + * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments * @param {String} textComment - * @returns {Number} + * @returns {Number} The comment's total length as seen from the backend */ function getCommentLength(textComment) { - return textComment.replace(/[^ -~]/g, '\\u????').length; + return getParsedComment(textComment).replace(/[^ -~]/g, '\\u????').trim().length; } /** diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 688b691fb62c..02bd202c1d2c 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -22,7 +22,7 @@ import MenuItem from '../components/MenuItem'; import Text from '../components/Text'; import CONST from '../CONST'; import reportPropTypes from './reportPropTypes'; -import withReportOrNavigateHome from './home/report/withReportOrNavigateHome'; +import withReportOrNotFound from './home/report/withReportOrNotFound'; import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView'; const propTypes = { @@ -181,7 +181,7 @@ ReportDetailsPage.propTypes = propTypes; export default compose( withLocalize, - withReportOrNavigateHome, + withReportOrNotFound, withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS, diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 07c4d62b448a..eb682d5026be 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -19,7 +19,7 @@ import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; import compose from '../libs/compose'; import * as ReportUtils from '../libs/ReportUtils'; import reportPropTypes from './reportPropTypes'; -import withReportOrNavigateHome from './home/report/withReportOrNavigateHome'; +import withReportOrNotFound from './home/report/withReportOrNotFound'; import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView'; const propTypes = { @@ -118,7 +118,7 @@ ReportParticipantsPage.displayName = 'ReportParticipantsPage'; export default compose( withLocalize, - withReportOrNavigateHome, + withReportOrNotFound, withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS, diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index 86fa8b6cec16..fca21166d0f6 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -20,7 +20,7 @@ import Picker from '../components/Picker'; import * as ValidationUtils from '../libs/ValidationUtils'; import OfflineWithFeedback from '../components/OfflineWithFeedback'; import reportPropTypes from './reportPropTypes'; -import withReportOrNavigateHome from './home/report/withReportOrNavigateHome'; +import withReportOrNotFound from './home/report/withReportOrNotFound'; import Form from '../components/Form'; import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView'; @@ -220,7 +220,7 @@ ReportSettingsPage.propTypes = propTypes; export default compose( withLocalize, - withReportOrNavigateHome, + withReportOrNotFound, withOnyx({ policies: { key: ONYXKEYS.COLLECTION.POLICY, diff --git a/src/pages/ValidateLoginPage/index.website.js b/src/pages/ValidateLoginPage/index.website.js index ee8e1f38b04c..a15e13f232a9 100644 --- a/src/pages/ValidateLoginPage/index.website.js +++ b/src/pages/ValidateLoginPage/index.website.js @@ -82,14 +82,14 @@ class ValidateLoginPage extends Component { isAuthenticated = () => Boolean(lodashGet(this.props, 'session.authToken', null)); /** - * Where SignIn was initiated on the current browser. + * Whether SignIn was initiated on the current browser. * @returns {Boolean} */ - isSignInInitiated = () => !this.isAuthenticated() && this.props.credentials && this.props.credentials.login; + isSignInInitiated = () => !this.isAuthenticated() && lodashGet(this.props, 'credentials.login', null); render() { return ( - this.isOnPasswordlessBeta() + this.isOnPasswordlessBeta() && !this.isSignInInitiated() && !lodashGet(this.props, 'account.isLoading', true) ? ( CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.hasExceededMaxCommentLength; return ( )} - + e.preventDefault()} + > e.preventDefault()} disabled={this.state.isCommentEmpty || isBlockedFromConcierge || this.props.disabled || hasExceededMaxCommentLength} hitSlop={{ top: 3, right: 3, bottom: 3, left: 3, @@ -709,7 +721,7 @@ class ReportActionCompose extends React.Component { > {!this.props.isSmallScreenWidth && } - + {this.state.isDraggingOver && } diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index abef7beb65ca..37369611f190 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -69,6 +69,7 @@ class ReportActionItemMessageEdit extends React.Component { this.triggerSaveOrCancel = this.triggerSaveOrCancel.bind(this); this.onSelectionChange = this.onSelectionChange.bind(this); this.addEmojiToTextBox = this.addEmojiToTextBox.bind(this); + this.setExceededMaxCommentLength = this.setExceededMaxCommentLength.bind(this); this.saveButtonID = 'saveButton'; this.cancelButtonID = 'cancelButton'; this.emojiButtonID = 'emojiButton'; @@ -84,6 +85,7 @@ class ReportActionItemMessageEdit extends React.Component { end: draftMessage.length, }, isFocused: false, + hasExceededMaxCommentLength: false, }; } @@ -96,6 +98,16 @@ class ReportActionItemMessageEdit extends React.Component { this.setState({selection: e.nativeEvent.selection}); } + /** + * Updates the composer when the comment length is exceeded + * Shows red borders and prevents the comment from being sent + * + * @param {Boolean} hasExceededMaxCommentLength + */ + setExceededMaxCommentLength(hasExceededMaxCommentLength) { + this.setState({hasExceededMaxCommentLength}); + } + /** * Update the value of the draft in Onyx * @@ -217,8 +229,7 @@ class ReportActionItemMessageEdit extends React.Component { } render() { - const draftLength = ReportUtils.getCommentLength(this.state.draft); - const hasExceededMaxCommentLength = draftLength > CONST.MAX_COMMENT_LENGTH; + const hasExceededMaxCommentLength = this.state.hasExceededMaxCommentLength; return ( - + ); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 29fea5e2ed19..06f39e777c8a 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -3,7 +3,6 @@ import _ from 'underscore'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; import {View, Keyboard} from 'react-native'; -import lodashFindLast from 'lodash/findLast'; import CONST from '../../../CONST'; import ReportActionCompose from './ReportActionCompose'; @@ -18,6 +17,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../../componen import styles from '../../../styles/styles'; import reportActionPropTypes from './reportActionPropTypes'; import reportPropTypes from '../../reportPropTypes'; +import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; const propTypes = { /** Report object for the current report */ @@ -70,7 +70,7 @@ class ReportFooter extends React.Component { const isArchivedRoom = ReportUtils.isArchivedRoom(this.props.report); let reportClosedAction; if (isArchivedRoom) { - reportClosedAction = lodashFindLast(this.props.reportActions, action => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED); + reportClosedAction = ReportActionsUtils.getLastClosedReportAction(this.props.reportActions); } const hideComposer = isArchivedRoom || !_.isEmpty(this.props.errors); return ( diff --git a/src/pages/home/report/withReportOrNavigateHome.js b/src/pages/home/report/withReportOrNotFound.js similarity index 68% rename from src/pages/home/report/withReportOrNavigateHome.js rename to src/pages/home/report/withReportOrNotFound.js index 5e74f65a0cb8..e1e3b0f64ef0 100644 --- a/src/pages/home/report/withReportOrNavigateHome.js +++ b/src/pages/home/report/withReportOrNotFound.js @@ -3,7 +3,7 @@ import React, {Component} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import getComponentDisplayName from '../../../libs/getComponentDisplayName'; -import Navigation from '../../../libs/Navigation/Navigation'; +import NotFoundPage from '../../ErrorPage/NotFoundPage'; import ONYXKEYS from '../../../ONYXKEYS'; import reportPropTypes from '../../reportPropTypes'; @@ -22,15 +22,12 @@ export default function (WrappedComponent) { report: {}, }; - class WithReportOrNavigateHome extends Component { - componentDidMount() { - if (!_.isEmpty(this.props.report)) { - return; + class WithReportOrNotFound extends Component { + render() { + if (_.isEmpty(this.props.report) || !this.props.report.reportID) { + return ; } - Navigation.dismissModal(); - } - render() { const rest = _.omit(this.props, ['forwardedRef']); return ( @@ -43,17 +40,18 @@ export default function (WrappedComponent) { } } - WithReportOrNavigateHome.propTypes = propTypes; - WithReportOrNavigateHome.defaultProps = defaultProps; - WithReportOrNavigateHome.displayName = `withReportOrNavigateHome(${getComponentDisplayName(WrappedComponent)})`; - const withReportOrNavigateHome = React.forwardRef((props, ref) => ( + WithReportOrNotFound.propTypes = propTypes; + WithReportOrNotFound.defaultProps = defaultProps; + WithReportOrNotFound.displayName = `withReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`; + // eslint-disable-next-line rulesdir/no-negated-variables + const withReportOrNotFound = React.forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading - + )); return withOnyx({ report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`, }, - })(withReportOrNavigateHome); + })(withReportOrNotFound); } diff --git a/src/pages/iou/steps/IOUConfirmPage.js b/src/pages/iou/steps/IOUConfirmPage.js index a4f4ce866f60..9cc511990697 100644 --- a/src/pages/iou/steps/IOUConfirmPage.js +++ b/src/pages/iou/steps/IOUConfirmPage.js @@ -27,7 +27,7 @@ const propTypes = { login: PropTypes.string.isRequired, alternateText: PropTypes.string, hasDraftComment: PropTypes.bool, - icons: PropTypes.arrayOf(PropTypes.string), + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), searchText: PropTypes.string, text: PropTypes.string, keyForList: PropTypes.string, diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js index 38d5c7a9d46d..185b37d85bb1 100644 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsPage.js @@ -23,7 +23,7 @@ const propTypes = { login: PropTypes.string.isRequired, alternateText: PropTypes.string, hasDraftComment: PropTypes.bool, - icons: PropTypes.arrayOf(PropTypes.string), + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), searchText: PropTypes.string, text: PropTypes.string, keyForList: PropTypes.string, diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js index f43325b6bdc8..8239ded8468a 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js @@ -29,7 +29,7 @@ const propTypes = { login: PropTypes.string.isRequired, alternateText: PropTypes.string, hasDraftComment: PropTypes.bool, - icons: PropTypes.arrayOf(PropTypes.string), + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), searchText: PropTypes.string, text: PropTypes.string, keyForList: PropTypes.string, diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 95a58c26fa8a..61e283b51aab 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -11,7 +11,7 @@ export default PropTypes.shape({ hasOutstandingIOU: PropTypes.bool, /** List of icons for report participants */ - icons: PropTypes.arrayOf(PropTypes.string), + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), /** Are we loading more report actions? */ isLoadingMoreReportActions: PropTypes.bool, diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.js index e6692a107b1f..aeadeb32a88a 100644 --- a/src/pages/settings/Profile/PronounsPage.js +++ b/src/pages/settings/Profile/PronounsPage.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; -import React from 'react'; +import React, {Component} from 'react'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails'; import ScreenWrapper from '../../../components/ScreenWrapper'; import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; @@ -11,10 +11,10 @@ import styles from '../../../styles/styles'; import Navigation from '../../../libs/Navigation/Navigation'; import * as PersonalDetails from '../../../libs/actions/PersonalDetails'; import compose from '../../../libs/compose'; -import OptionsList from '../../../components/OptionsList'; import themeColors from '../../../styles/themes/default'; import * as Expensicons from '../../../components/Icon/Expensicons'; import CONST from '../../../CONST'; +import OptionsSelector from '../../../components/OptionsSelector'; const greenCheckmark = {src: Expensicons.Checkmark, color: themeColors.success}; @@ -27,60 +27,119 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -const PronounsPage = (props) => { - const currentPronouns = lodashGet(props.currentUserPersonalDetails, 'pronouns', ''); - const pronounsList = _.map(props.translate('pronouns'), (value, key) => { - const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${key}`; - return { - text: value, - value: fullPronounKey, - keyForList: key, +class PronounsPage extends Component { + constructor(props) { + super(props); - // Include the green checkmark icon to indicate the currently selected value - customIcon: fullPronounKey === currentPronouns ? greenCheckmark : undefined, + this.loadPronouns = this.loadPronouns.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.getFilteredPronouns = this.getFilteredPronouns.bind(this); - // This property will make the currently selected value have bold text - boldStyle: fullPronounKey === currentPronouns, + this.loadPronouns(); + this.state = { + searchValue: '', }; - }); + } + + componentDidUpdate(prevProps) { + // If the pronouns have changed, we need to update the pronouns list because refreshing the page + // breaks the component lifecycle, so we need to "manually" reset the component. + if (prevProps.currentUserPersonalDetails.pronouns === this.props.currentUserPersonalDetails.pronouns) { + return; + } + + this.onChangeText(); + this.loadPronouns(); + } + + onChangeText(searchValue = '') { + this.setState({searchValue}); + } /** - * @param {String} selectedPronouns + * Returns the pronouns list filtered by searchValue needed for the OptionsSelector. + * Empty array is returned if the searchValue is empty. + * + * @returns {Array} */ - const updatePronouns = (selectedPronouns) => { - PersonalDetails.updatePronouns(selectedPronouns); - }; - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - <> - Navigation.navigate(ROUTES.SETTINGS_PROFILE)} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - - {props.translate('pronounsPage.isShownOnProfile')} - - updatePronouns(option.value)} - hideSectionHeaders - optionHoveredStyle={styles.hoveredComponentBG} - shouldHaveOptionSeparator - contentContainerStyles={[styles.ph5, safeAreaPaddingBottomStyle]} - /> - - )} - - ); -}; + getFilteredPronouns() { + const searchedValue = this.state.searchValue.trim(); + if (searchedValue.length === 0) { + return []; + } + return _.filter(this.pronounsList, + pronous => pronous.text.toLowerCase().indexOf(searchedValue.toLowerCase()) >= 0); + } + + /** + * Loads the pronouns list from the translations and adds the green checkmark icon to the currently selected value. + * + * @returns {void} + */ + loadPronouns() { + const currentPronouns = lodashGet(this.props.currentUserPersonalDetails, 'pronouns', ''); + + this.pronounsList = _.map(this.props.translate('pronouns'), (value, key) => { + const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${key}`; + const isCurrentPronouns = fullPronounKey === currentPronouns; + + return { + text: value, + value: fullPronounKey, + keyForList: key, + + // Include the green checkmark icon to indicate the currently selected value + customIcon: isCurrentPronouns ? greenCheckmark : undefined, + + // This property will make the currently selected value have bold text + boldStyle: isCurrentPronouns, + }; + }); + } + + /** + * @param {Object} selectedPronouns + */ + updatePronouns(selectedPronouns) { + PersonalDetails.updatePronouns(selectedPronouns.value); + } + + render() { + const filteredPronounsList = this.getFilteredPronouns(); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + <> + Navigation.navigate(ROUTES.SETTINGS_PROFILE)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + {this.props.translate('pronounsPage.isShownOnProfile')} + + + + )} + + ); + } +} PronounsPage.propTypes = propTypes; PronounsPage.defaultProps = defaultProps; -PronounsPage.displayName = 'PronounsPage'; export default compose( withLocalize, diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js index 1a6503457a9f..0878affa2e6e 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.js @@ -33,28 +33,74 @@ class TimezoneSelectPage extends Component { this.saveSelectedTimezone = this.saveSelectedTimezone.bind(this); this.filterShownTimezones = this.filterShownTimezones.bind(this); + this.getTimezoneOption = this.getTimezoneOption.bind(this); - this.currentSelectedTimezone = lodashGet(props.currentUserPersonalDetails, 'timezone.selected', CONST.DEFAULT_TIME_ZONE.selected); + this.timezone = this.getUserTimezone(props.currentUserPersonalDetails); this.allTimezones = _.chain(moment.tz.names()) .filter(timezone => !timezone.startsWith('Etc/GMT')) - .map(timezone => ({ - text: timezone, - keyForList: timezone, - - // Include the green checkmark icon to indicate the currently selected value - customIcon: timezone === this.currentSelectedTimezone ? greenCheckmark : undefined, - - // This property will make the currently selected value have bold text - boldStyle: timezone === this.currentSelectedTimezone, - })) + .map(this.getTimezoneOption) .value(); this.state = { - timezoneInputText: this.currentSelectedTimezone, + timezoneInputText: this.timezone.selected, + timezoneOptions: this.allTimezones, + }; + } + + componentDidUpdate() { + // componentDidUpdate is added in order to update the timezone options when automatic is toggled on/off as + // navigating back doesn't unmount the page, thus it won't update the timezone options & stay disabled without this. + const newTimezone = this.getUserTimezone(this.props.currentUserPersonalDetails); + if (_.isEqual(this.timezone, newTimezone)) { + return; + } + this.timezone = newTimezone; + this.allTimezones = _.map(this.allTimezones, (timezone) => { + const text = timezone.text.split('-')[0]; + return this.getTimezoneOption(text); + }); + + this.setState({ + timezoneInputText: this.timezone.selected, timezoneOptions: this.allTimezones, + }); + } + + /** + * We add the current time to the key to fix a bug where the list options don't update unless the key is updated. + * @param {String} text + * @return {string} key for list item + */ + getKey(text) { + return `${text}-${(new Date()).getTime()}`; + } + + /** + * Get timezone option object for the list. + * @param {String} text + * @return {Object} Timezone list option + */ + getTimezoneOption(text) { + return { + text, + keyForList: this.getKey(text), + + // Include the green checkmark icon to indicate the currently selected value + customIcon: text === this.timezone.selected ? greenCheckmark : undefined, + + // This property will make the currently selected value have bold text + boldStyle: text === this.timezone.selected, }; } + /** + * @param {Object} currentUserPersonalDetails + * @return {Object} user's timezone data + */ + getUserTimezone(currentUserPersonalDetails) { + return lodashGet(currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); + } + /** * @param {Object} timezone * @param {String} timezone.text @@ -90,7 +136,7 @@ class TimezoneSelectPage extends Component { onChangeText={this.filterShownTimezones} onSelectRow={this.saveSelectedTimezone} optionHoveredStyle={styles.hoveredComponentBG} - sections={[{data: this.state.timezoneOptions, indexOffset: 0}]} + sections={[{data: this.state.timezoneOptions, indexOffset: 0, isDisabled: this.timezone.automatic}]} shouldHaveOptionSeparator safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} initiallyFocusedOptionKey={this.currentSelectedTimezone} diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 9ae596c2fc7c..96056d68acd8 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -70,6 +70,7 @@ class BaseValidateCodeForm extends React.Component { formError: {}, validateCode: props.credentials.validateCode || '', twoFactorAuthCode: '', + linkSent: false, }; } @@ -131,6 +132,9 @@ class BaseValidateCodeForm extends React.Component { } this.setState({formError: {}}); User.resendValidateCode(this.props.credentials.login, true); + + // Give feedback to the user to let them know the email was sent so they don't spam the button. + this.setState({linkSent: true}); } /** @@ -212,15 +216,21 @@ class BaseValidateCodeForm extends React.Component { autoFocus /> - - - {this.props.translate('validateCodeForm.magicCodeNotReceived')} + {this.state.linkSent ? ( + + {this.props.account.message} - + ) : ( + + + {this.props.translate('validateCodeForm.magicCodeNotReceived')} + + + )} )} diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 7b02220bb102..5f5c9a9ee35c 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -29,7 +29,6 @@ import OfflineWithFeedback from '../../components/OfflineWithFeedback'; import {withNetwork} from '../../components/OnyxProvider'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import networkPropTypes from '../../components/networkPropTypes'; -import * as Expensicons from '../../components/Icon/Expensicons'; import * as ReportUtils from '../../libs/ReportUtils'; import FormHelpMessage from '../../components/FormHelpMessage'; import TextInput from '../../components/TextInput'; @@ -309,7 +308,7 @@ class WorkspaceMembersPage extends React.Component { if (email !== this.props.session.email && email !== this.props.policy.owner && policyMember.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { removableMembers.push(email); } - const details = lodashGet(this.props.personalDetails, email, {displayName: email, login: email, avatar: Expensicons.FallbackAvatar}); + const details = lodashGet(this.props.personalDetails, email, {displayName: email, login: email}); data.push({ ...policyMember, ...details, diff --git a/src/styles/emojiHeaderContainerPlatformStyles/index.js b/src/styles/emojiHeaderContainerPlatformStyles/index.js new file mode 100644 index 000000000000..6bc0c37459ce --- /dev/null +++ b/src/styles/emojiHeaderContainerPlatformStyles/index.js @@ -0,0 +1,6 @@ +// This change is only temporary until browser have fixed the issue with position: sticky causing the header to bounce on scroll in non-native apps +// https://github.com/Expensify/App/issues/15282 +// https://bugs.chromium.org/p/chromium/issues/detail?id=734461 +export default { + top: -1, +}; diff --git a/src/styles/emojiHeaderContainerPlatformStyles/index.native.js b/src/styles/emojiHeaderContainerPlatformStyles/index.native.js new file mode 100644 index 000000000000..ff8b4c56321a --- /dev/null +++ b/src/styles/emojiHeaderContainerPlatformStyles/index.native.js @@ -0,0 +1 @@ +export default {}; diff --git a/src/styles/styles.js b/src/styles/styles.js index 0f8457940c87..50104821d8fa 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -15,6 +15,7 @@ import codeStyles from './codeStyles'; import visibility from './utilities/visibility'; import writingDirection from './utilities/writingDirection'; import optionAlternateTextPlatformStyles from './optionAlternateTextPlatformStyles'; +import emojiHeaderContainerPlatformStyles from './emojiHeaderContainerPlatformStyles'; import pointerEventsNone from './pointerEventsNone'; import pointerEventsAuto from './pointerEventsAuto'; import overflowXHidden from './overflowXHidden'; @@ -1222,7 +1223,7 @@ const styles = { minWidth: 'auto', flexBasis: 'auto', flexGrow: 0, - flexShrink: 0, + flexShrink: 1, }, displayNameTooltipEllipsis: { @@ -1488,6 +1489,7 @@ const styles = { height: CONST.EMOJI_PICKER_HEADER_HEIGHT, justifyContent: 'center', width: '100%', + ...emojiHeaderContainerPlatformStyles, }, emojiSkinToneTitle: { @@ -2839,7 +2841,8 @@ const styles = { borderRadius: 10, overflow: 'hidden', paddingVertical: 2, - flexShrink: 1, + flexShrink: 0, + maxWidth: variables.badgeMaxWidth, fontSize: variables.fontSizeSmall, ...spacing.ph2, }, diff --git a/src/styles/variables.js b/src/styles/variables.js index 0e91a707d00a..00ac9cf6ae5e 100644 --- a/src/styles/variables.js +++ b/src/styles/variables.js @@ -98,4 +98,5 @@ export default { modalTopBigIconHeight: 244, modalWordmarkWidth: 154, modalWordmarkHeight: 34, + badgeMaxWidth: 180, };