diff --git a/.env.staging b/.env.staging index c789087ebded..c0daddc3420e 100644 --- a/.env.staging +++ b/.env.staging @@ -7,3 +7,4 @@ PUSHER_APP_KEY=268df511a204fbb60884 USE_WEB_PROXY=false ENVIRONMENT=staging SEND_CRASH_REPORTS=true +INLINE_RUNTIME_CHUNK=false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0dd10af71747..d7e35bb5894d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -68,7 +68,6 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct) - [ ] I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline) - [ ] I tested this PR with a [High Traffic account](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#high-traffic-accounts) against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability). - - [ ] I included screenshots or videos for tests on [all platforms](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms) - [ ] I ran the tests on **all platforms** & verified they passed on: - [ ] iOS / native @@ -167,23 +166,45 @@ The reviewer will copy/paste it into a new comment and complete it after the aut -### Screenshots - +### Screenshots/Videos +
+Web + + + +
+ +
+Mobile Web - Chrome + + + +
+ +
+Mobile Web - Safari + + + +
+ +
+Desktop + + -#### Web - +
+ +
+iOS -#### Mobile Web - Chrome - + -#### Mobile Web - Safari - +
-#### Desktop - +
+Android -#### iOS - + -#### Android - +
diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 95e187ff4d3a..d475acf5380f 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -4,10 +4,6 @@ description: Set up Node runs: using: composite steps: - - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - with: - fetch-depth: 0 - - uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 with: node-version-file: '.nvmrc' diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index a594fa69d1a1..962c12a13ac1 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -66,7 +66,7 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) { for (let i = 0; i < combinedComments.length; i++) { // Skip all other comments if we already found the reviewer checklist if (foundReviewerChecklist) { - return; + break; } const whitespace = /([\n\r])/gm; diff --git a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.js b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.js index 7fb27e5b5a5a..1f942ce66fd6 100644 --- a/.github/actions/javascript/reviewerChecklist/reviewerChecklist.js +++ b/.github/actions/javascript/reviewerChecklist/reviewerChecklist.js @@ -56,7 +56,7 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) { for (let i = 0; i < combinedComments.length; i++) { // Skip all other comments if we already found the reviewer checklist if (foundReviewerChecklist) { - return; + break; } const whitespace = /([\n\r])/gm; diff --git a/.github/workflows/e2ePerformanceRegressionTests.yml b/.github/workflows/e2ePerformanceRegressionTests.yml index a59eb8c0dbff..ad6425d4ec90 100644 --- a/.github/workflows/e2ePerformanceRegressionTests.yml +++ b/.github/workflows/e2ePerformanceRegressionTests.yml @@ -7,10 +7,15 @@ on: jobs: e2e-tests: if: ${{ github.event.label.name == 'e2e' }} - name: "Run e2e performance regression tests" + name: Run e2e performance regression tests # Although the tests will run on an android emulator, using macOS as its more performant runs-on: macos-12 steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 @@ -43,7 +48,7 @@ jobs: disable-animations: false script: echo "Generated AVD snapshot for caching." -# Note: if the android build fails the logs can be incomplete. It can help to run the build once manually to get a full log + # Note: if the android build fails the logs can be incomplete. It can help to run the build once manually to get a full log - name: Preheat build system env: JAVA_HOME: ${{ env.JAVA_HOME_11_X64 }} @@ -76,4 +81,3 @@ jobs: name: test-failure-logs path: e2e/.results retention-days: 5 - diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0af8907b085d..f0bcf63b6955 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -11,6 +11,11 @@ jobs: if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} runs-on: ubuntu-latest steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - run: npm run lint diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 6aa397758ea2..5b61a294ca5b 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -31,6 +31,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: ubuntu-latest steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 @@ -100,6 +105,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: macos-12 steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Decrypt Developer ID Certificate @@ -135,6 +145,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: macos-12 steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 @@ -219,6 +234,11 @@ jobs: if: ${{ fromJSON(needs.validateActor.outputs.IS_DEPLOYER) }} runs-on: ubuntu-latest steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Setup Cloudflare CLI @@ -340,6 +360,11 @@ jobs: if: ${{ always() }} needs: [android, desktop, iOS, web] steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - name: Set version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 90e6e06e75d2..72dde5fe38cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,11 @@ jobs: chunk: ${{fromJson(needs.config.outputs.MATRIX)}} steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main # If automatic signing is enabled, iOS builds will fail, so ensure we always have the proper profile specified diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml new file mode 100644 index 000000000000..c6ac7cd7e964 --- /dev/null +++ b/.github/workflows/testBuild.yml @@ -0,0 +1,211 @@ +name: Build and deploy apps for testing + +on: + workflow_dispatch: + pull_request_target: + types: [opened, synchronize] + +env: + DEVELOPER_DIR: /Applications/Xcode_14.0.1.app/Contents/Developer + +jobs: + validateActor: + runs-on: ubuntu-latest + outputs: + IS_TEAM_MEMBER: ${{ fromJSON(steps.isUserDeployer.outputs.isTeamMember) }} + steps: + - id: isUserDeployer + uses: tspascoal/get-user-teams-membership@baf2e6adf4c3b897bd65a7e3184305c165aec872 + with: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + username: ${{ github.actor }} + team: mobile-deployers + + android: + name: Build and deploy Android for testing + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }} + runs-on: ubuntu-latest + steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + with: + ruby-version: '2.7' + bundler-cache: true + + - name: Decrypt keystore + run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output my-upload-key.keystore my-upload-key.keystore.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt json key + run: cd android/app && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Run Fastlane beta test + id: runFastlaneBetaTest + run: bundle exec fastlane android build_internal + env: + S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} + S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_BUCKET: ad-hoc-expensify-cash + S3_REGION: us-east-1 + PULL_REQUEST_NUMBER: ${{ github.event.number }} + + - uses: actions/upload-artifact@v3 + with: + name: android + path: ./android_paths.json + + iOS: + name: Build and deploy iOS for testing + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }} + runs-on: macos-12 + steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - uses: ruby/setup-ruby@eae47962baca661befdfd24e4d6c34ade04858f7 + with: + ruby-version: '2.7' + bundler-cache: true + + - name: Install cocoapods + uses: nick-invision/retry@0711ba3d7808574133d713a0d92d2941be03a350 + with: + timeout_minutes: 10 + max_attempts: 5 + command: cd ios && pod install + + - name: Decrypt profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output chat_expensify_adhoc.mobileprovision chat_expensify_adhoc.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt certificate + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Run Fastlane + run: bundle exec fastlane ios build_internal + env: + S3_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY_ID }} + S3_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_BUCKET: ad-hoc-expensify-cash + S3_REGION: us-east-1 + PULL_REQUEST_NUMBER: ${{ github.event.number }} + + - uses: actions/upload-artifact@v3 + with: + name: ios + path: ./ios_paths.json + + desktop: + name: Build and deploy Desktop for testing + needs: validateActor + if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }} + runs-on: macos-12 + steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + + - uses: Expensify/App/.github/actions/composite/setupNode@main + + - name: Decrypt Developer ID Certificate + run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg + env: + DEVELOPER_ID_SECRET_PASSPHRASE: ${{ secrets.DEVELOPER_ID_SECRET_PASSPHRASE }} + + - name: Build desktop app for testing + run: npm run desktop-build-internal -- --publish always + env: + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # web: + # name: Build and deploy Web + # needs: validateActor + # if: ${{ fromJSON(needs.validateActor.outputs.IS_TEAM_MEMBER) }} + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + # with: + # fetch-depth: 0 + # ref: ${{ github.event.pull_request.head.sha }} + + # - uses: Expensify/App/.github/actions/composite/setupNode@main + + # - name: Configure AWS Credentials + # # Version: 1.5.5 + # uses: aws-actions/configure-aws-credentials@e97d7fbc8e0e5af69631c13daa0f4b5a8d88165b + # with: + # aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + # aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # aws-region: us-east-1 + + # - name: Build web for staging + # run: npm run build-staging + + # - name: Build docs + # run: npm run storybook-build + # continue-on-error: true + + # - name: Deploy to S3 for internal testing + # run: aws s3 cp --recursive --acl public-read "$GITHUB_WORKSPACE"/dist s3://ad-hoc-expensify-cash/web/"$PULL_REQUEST_NUMBER" + # env: + # PULL_REQUEST_NUMBER: ${{ github.event.number }} + + postGithubComment: + runs-on: ubuntu-latest + name: Post a GitHub comment with app download links for testing + needs: [android, ios] + steps: + - name: Checkout + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - uses: actions/download-artifact@v3 + + - name: Read JSONs with paths + id: set_var + run: | + content_android="$(cat ./android/android_paths.json)" + content_android="${content_android//'%'/'%25'}" + content_android="${content_android//$'\n'/'%0A'}" + content_android="${content_android//$'\r'/'%0D'}" + echo "android_paths=$content_android" >> "$GITHUB_OUTPUT" + content_ios="$(cat ./ios/ios_paths.json)" + content_ios="${content_ios//'%'/'%25'}" + content_ios="${content_ios//$'\n'/'%0A'}" + content_ios="${content_ios//$'\r'/'%0D'}" + echo "ios_paths=$content_ios" >> "$GITHUB_OUTPUT" + + - name: Publish links to apps for download + run: | + gh pr comment --body \ + "Use the links below to test this build in android and iOS. Happy testing! + | android :robot: | iOS :apple: | + | ------------- | ------------- | + | ${{fromJson(steps.set_var.outputs.android_paths).html_path}} | ${{fromJson(steps.set_var.outputs.ios_paths).html_path}} |" + env: + GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} diff --git a/.github/workflows/validateGithubActions.yml b/.github/workflows/validateGithubActions.yml index 4e51d785d6b0..b583ba31f2bc 100644 --- a/.github/workflows/validateGithubActions.yml +++ b/.github/workflows/validateGithubActions.yml @@ -10,6 +10,11 @@ jobs: if: github.actor != 'OSBotify' runs-on: ubuntu-latest steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main # Rebuild all the actions on this branch and check for a diff. Fail if there is one, diff --git a/.github/workflows/verifyPodfile.yml b/.github/workflows/verifyPodfile.yml index 17d80ca7e055..84f52d09997a 100644 --- a/.github/workflows/verifyPodfile.yml +++ b/.github/workflows/verifyPodfile.yml @@ -10,6 +10,11 @@ jobs: if: github.actor != 'OSBotify' runs-on: ubuntu-latest steps: + # This action checks-out the repository, so the workflow can access it. + - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + with: + fetch-depth: 0 + - uses: Expensify/App/.github/actions/composite/setupNode@main - run: ./.github/scripts/verifyPodfile.sh diff --git a/.github/workflows/warnCPLabel.yml b/.github/workflows/warnCPLabel.yml index 1d57ae38f718..c488bb1836d6 100644 --- a/.github/workflows/warnCPLabel.yml +++ b/.github/workflows/warnCPLabel.yml @@ -16,9 +16,9 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} body: | - :warning: :warning: **Heads up! This pull request has the `CP Staging` label.** :warning: :warning: - Merging it will cause it to be immediately deployed to staging, _even if the [open `StagingDeployCash` deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) is locked._ - + :warning: :warning: **Heads up! This pull request has the `CP Staging` label** :warning: :warning: + If you applied the `CP Staging` label before the PR was merged, the PR will be be immediately deployed to staging even if the [open `StagingDeployCash` deploy checklist](https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3AStagingDeployCash) is locked. + However if you applied the `CP Staging` after the PR was merged it's possible it won't be CP'ed automatically. If you need it to be CP'ed to staging, tag a member of @Expensify/mobile-deployers to CP it manually, otherwise you can wait for it to go out with the next deploy. - if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main with: diff --git a/Gemfile b/Gemfile index c41a21c6604f..d8b9c135680d 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,7 @@ source "https://rubygems.org" gem "cocoapods", "~> 1.11.3" gem "fastlane", "~> 2" gem "xcpretty", "~> 0" + + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/Gemfile.lock b/Gemfile.lock index 5ac763189187..95a2611d953c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,8 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + apktools (0.7.4) + rubyzip (~> 2.0) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) @@ -155,6 +157,10 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-aws_s3 (2.1.0) + apktools (~> 0.7) + aws-sdk-s3 (~> 1) + mime-types (~> 3.3) ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) @@ -207,6 +213,9 @@ GEM json (2.6.2) jwt (2.5.0) memoist (0.16.2) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) mini_magick (4.11.0) mini_mime (1.1.2) minitest (5.16.2) @@ -283,6 +292,7 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.11.3) fastlane (~> 2) + fastlane-plugin-aws_s3 xcpretty (~> 0) BUNDLED WITH diff --git a/README.md b/README.md index d456c5e52af4..61734c2417ed 100644 --- a/README.md +++ b/README.md @@ -293,7 +293,7 @@ export default withOnyx({ This application is built with the following principles. 1. **Data Flow** - Ideally, this is how data flows through the app: 1. Server pushes data to the disk of any client (Server -> Pusher event -> Action listening to pusher event -> Onyx). - >**Note:** Currently the code only does this with report comments. Until we make more server changes, this steps is actually done by the client requesting data from the server via XHR and then storing the response in Onyx. + >**Note:** Currently the code only does this with report comments. Until we make more server changes, this step is actually done by the client requesting data from the server via XHR and then storing the response in Onyx. 2. Disk pushes data to the UI (Onyx -> withOnyx() -> React component). 3. UI pushes data to people's brains (React component -> device screen). 4. Brain pushes data into UI inputs (Device input -> React component). diff --git a/android/app/build.gradle b/android/app/build.gradle index 99d297b5812b..b24a60ac5067 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 1001022907 - versionName "1.2.29-7" + versionCode 1001023301 + versionName "1.2.33-1" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { @@ -258,6 +258,11 @@ android { matchingFallbacks = ['release'] signingConfig signingConfigs.debug } + internalRelease { + initWith release + matchingFallbacks = ['release'] + signingConfig signingConfigs.debug + } } // applicationVariants are e.g. debug, release diff --git a/android/app/src/debug/assets/airshipconfig.properties b/android/app/src/debug/assets/airshipconfig.properties new file mode 100644 index 000000000000..490f74552f11 --- /dev/null +++ b/android/app/src/debug/assets/airshipconfig.properties @@ -0,0 +1,8 @@ +appKey = uulSSfTDQJ2r0PMpjRrhmQ +appSecret = D4Bhf0HrQEehrPua74Tyiw +inProduction = false +developmentLogLevel = VERBOSE + +# Notification Customization +notificationIcon = ic_notification +notificationAccentColor = #2EAAE2 \ No newline at end of file diff --git a/android/app/src/e2eRelease/assets/airshipconfig.properties b/android/app/src/e2eRelease/assets/airshipconfig.properties new file mode 100644 index 000000000000..194c4577de8b --- /dev/null +++ b/android/app/src/e2eRelease/assets/airshipconfig.properties @@ -0,0 +1,7 @@ +appKey = 55vypj0ARc6cN09MX7ogtQ +appSecret = EsSaqbdLSvmyC6kSBFJCtQ +inProduction = true + +# Notification Customization +notificationIcon = ic_notification +notificationAccentColor = #2EAAE2 \ No newline at end of file diff --git a/android/app/src/main/assets/airshipconfig.properties b/android/app/src/main/assets/airshipconfig.properties deleted file mode 100644 index 6e37dfb7465e..000000000000 --- a/android/app/src/main/assets/airshipconfig.properties +++ /dev/null @@ -1,9 +0,0 @@ -developmentAppKey = uulSSfTDQJ2r0PMpjRrhmQ -developmentAppSecret = D4Bhf0HrQEehrPua74Tyiw - -productionAppKey = 55vypj0ARc6cN09MX7ogtQ -productionAppSecret = EsSaqbdLSvmyC6kSBFJCtQ - -# Notification Customization -notificationIcon = ic_notification -notificationAccentColor = #2EAAE2 diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png index eb4a29a5eb31..7612112d1bc5 100644 Binary files a/android/app/src/main/res/drawable-hdpi/ic_notification.png and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png index 6bd66d53e897..89accf5424f8 100644 Binary files a/android/app/src/main/res/drawable-mdpi/ic_notification.png and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png index 35220f20eb97..a01f2c5e0dc9 100644 Binary files a/android/app/src/main/res/drawable-xhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png index 344a154d7fe5..3bb969329c79 100644 Binary files a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png index 53d01824530d..697922b1e689 100644 Binary files a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png index 418344ef64a5..2dd985e8eeea 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-hdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png index 88c82f6938cf..7d0644eed692 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-mdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png index aad0a60610c8..f2dd3df6e1ad 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png index 855115119d1e..59538b46ded4 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xxhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png b/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png index 783b32cea53b..9a8a1bbf2640 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png and b/android/app/src/main/res/mipmap-xxxhdpi/bootsplash_logo.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 89436352feb8..35d8a1c3cef2 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 807ac49eb556..9a5989a435aa 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 21e97a75c38a..1b925f61ae28 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,5 +1,5 @@ - #FFFFFF + #061B09 #FFFFFF #0185ff #0b1b34 diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 0cad8b818b02..9057ea9c1cc2 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -7,15 +7,16 @@ diff --git a/android/app/src/release/assets/airshipconfig.properties b/android/app/src/release/assets/airshipconfig.properties new file mode 100644 index 000000000000..194c4577de8b --- /dev/null +++ b/android/app/src/release/assets/airshipconfig.properties @@ -0,0 +1,7 @@ +appKey = 55vypj0ARc6cN09MX7ogtQ +appSecret = EsSaqbdLSvmyC6kSBFJCtQ +inProduction = true + +# Notification Customization +notificationIcon = ic_notification +notificationAccentColor = #2EAAE2 \ No newline at end of file diff --git a/assets/images/expensify-logo-round-clearspace.png b/assets/images/expensify-logo-round-clearspace.png index 154e1cbfec1c..da0f19dda770 100644 Binary files a/assets/images/expensify-logo-round-clearspace.png and b/assets/images/expensify-logo-round-clearspace.png differ diff --git a/assets/images/expensify-logo-round.png b/assets/images/expensify-logo-round.png index 8904b9d80403..59d29ed09530 100644 Binary files a/assets/images/expensify-logo-round.png and b/assets/images/expensify-logo-round.png differ diff --git a/assets/images/expensify-wordmark.svg b/assets/images/expensify-wordmark.svg index 68f4231501c2..09fdaf2b4c83 100644 --- a/assets/images/expensify-wordmark.svg +++ b/assets/images/expensify-wordmark.svg @@ -1,9 +1,9 @@ - + \ No newline at end of file diff --git a/assets/images/new-expensify-dev.svg b/assets/images/new-expensify-dev.svg index 4346bdbaeef5..5e36ffebe0d3 100644 --- a/assets/images/new-expensify-dev.svg +++ b/assets/images/new-expensify-dev.svg @@ -1,12 +1,46 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/new-expensify-stg.svg b/assets/images/new-expensify-stg.svg index 26aff3d44293..61852bbb1932 100644 --- a/assets/images/new-expensify-stg.svg +++ b/assets/images/new-expensify-stg.svg @@ -1,12 +1,48 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg index cc9c02021485..dc8273e6aa99 100644 --- a/assets/images/new-expensify.svg +++ b/assets/images/new-expensify.svg @@ -1,10 +1,29 @@ - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__bank-arrow.svg b/assets/images/simple-illustrations/simple-illustration__bank-arrow.svg new file mode 100644 index 000000000000..7e5b4dae1ccc --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__bank-arrow.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__bill.svg b/assets/images/simple-illustrations/simple-illustration__bill.svg new file mode 100644 index 000000000000..7fb76fb8c09b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__bill.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg b/assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg new file mode 100644 index 000000000000..eeabc78b1881 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__concierge.svg b/assets/images/simple-illustrations/simple-illustration__concierge.svg new file mode 100644 index 000000000000..8275671c3486 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__concierge.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__credit-cards.svg b/assets/images/simple-illustrations/simple-illustration__credit-cards.svg new file mode 100644 index 000000000000..8e070f074ef3 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__credit-cards.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__invoice.svg b/assets/images/simple-illustrations/simple-illustration__invoice.svg new file mode 100644 index 000000000000..0a10b70a7bfe --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__invoice.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__lockopen.svg b/assets/images/simple-illustrations/simple-illustration__lockopen.svg new file mode 100644 index 000000000000..fb07d7a8628b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__lockopen.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__luggage.svg b/assets/images/simple-illustrations/simple-illustration__luggage.svg new file mode 100644 index 000000000000..9a01eee56662 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__luggage.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__money-receipts.svg b/assets/images/simple-illustrations/simple-illustration__money-receipts.svg new file mode 100644 index 000000000000..3d81f5dba653 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__money-receipts.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__moneybadge.svg b/assets/images/simple-illustrations/simple-illustration__moneybadge.svg new file mode 100644 index 000000000000..1f673aa20a90 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__moneybadge.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg b/assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg new file mode 100644 index 000000000000..3f89e6b14836 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__moneywings.svg b/assets/images/simple-illustrations/simple-illustration__moneywings.svg new file mode 100644 index 000000000000..b13abdf448af --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__moneywings.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__opensafe.svg b/assets/images/simple-illustrations/simple-illustration__opensafe.svg new file mode 100644 index 000000000000..273d68b62723 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__opensafe.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg b/assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg new file mode 100644 index 000000000000..623874d2d3eb --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__track-shoe.svg b/assets/images/simple-illustrations/simple-illustration__track-shoe.svg new file mode 100644 index 000000000000..5d45f2f9df67 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__track-shoe.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__treasurechest.svg b/assets/images/simple-illustrations/simple-illustration__treasurechest.svg new file mode 100644 index 000000000000..edb868db11d2 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__treasurechest.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/electronBuilder.config.js b/config/electronBuilder.config.js index 2c076e894032..6ffd9e3b2a8e 100644 --- a/config/electronBuilder.config.js +++ b/config/electronBuilder.config.js @@ -1,8 +1,23 @@ const {version} = require('../package.json'); -const isStaging = process.env.ELECTRON_ENV === 'staging'; const isPublishing = process.argv.includes('--publish'); +const s3Bucket = { + production: 'expensify-cash', + staging: 'staging-expensify-cash', + internal: 'ad-hoc-expensify-cash', +}; + +const macIcon = { + production: './desktop/icon.png', + staging: './desktop/icon-stg.png', + internal: './desktop/icon-stg.png', +}; + +const isCorrectElectronEnv = ['production', 'staging', 'internal'].includes( + process.env.ELECTRON_ENV, +); + /** * The configuration for the production and staging Electron builds. * It can be used to create local builds of the same, by omitting the `--publish` flag @@ -15,7 +30,9 @@ module.exports = { }, mac: { category: 'public.app-category.finance', - icon: isStaging ? './desktop/icon-stg.png' : './desktop/icon.png', + icon: isCorrectElectronEnv + ? macIcon[process.env.ELECTRON_ENV] + : './desktop/icon-stg.png', hardenedRuntime: true, entitlements: 'desktop/entitlements.mac.plist', entitlementsInherit: 'desktop/entitlements.mac.plist', @@ -26,16 +43,17 @@ module.exports = { artifactName: 'NewExpensify.dmg', internetEnabled: true, }, - publish: [{ - provider: 's3', - bucket: isStaging ? 'staging-expensify-cash' : 'expensify-cash', - channel: 'latest', - }], - afterSign: isPublishing ? './desktop/notarize.js' : undefined, - files: [ - 'dist', - '!dist/www/{.well-known,favicon*}', + publish: [ + { + provider: 's3', + bucket: isCorrectElectronEnv + ? s3Bucket[process.env.ELECTRON_ENV] + : 'ad-hoc-expensify-cash', + channel: 'latest', + }, ], + afterSign: isPublishing ? './desktop/notarize.js' : undefined, + files: ['dist', '!dist/www/{.well-known,favicon*}'], directories: { app: 'desktop', output: 'desktop-build', diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md index 87a309fc567c..ea4504d07f6a 100644 --- a/contributingGuides/CONTRIBUTING.md +++ b/contributingGuides/CONTRIBUTING.md @@ -69,6 +69,7 @@ This is the most common scenario for contributors. The Expensify team posts new It’s possible that you found a new bug or new feature that we haven’t posted as a job to the [GitHub repository](https://github.com/Expensify/App/issues?q=is%3Aissue). This is an opportunity to propose a job, and (optionally) a solution for that job. If it's a valid job proposal that we choose to implement by deploying it to production — either internally or via an external contributor — then we will compensate you $250 for identifying and proposing the bug or feature. If the bug or feature is fixed by a PR that is not associated with your proposal, then you will not be eligible for the corresponding compensation unless you can find the PR that fixed it and prove your proposal came first. - Note: If you get assigned the job you proposed **and** you complete the job, this $250 for identifying the improvement is *in addition to* the reward you will be paid for completing the job. - Note about proposed bugs or features: Expensify has the right not to pay the $250 reward if the suggested bug or feature is already planned. Currently, Expensify plans to implement all features of the old Expensify app in New Expensify. +- Note: whilst you may optionally propose a solution for that job on Slack, solutions are ultimately reviewed in GitHub. The onus is on you to propose the solution on GitHub, and/or ensure the issue creator will include a link to your proposal. Please follow these steps to propose a job: diff --git a/desktop/icon-dev.png b/desktop/icon-dev.png index bd8e57dfbe23..4589faa80d41 100644 Binary files a/desktop/icon-dev.png and b/desktop/icon-dev.png differ diff --git a/desktop/icon-stg.png b/desktop/icon-stg.png index 2a45484acb67..c605f8aea6ad 100644 Binary files a/desktop/icon-stg.png and b/desktop/icon-stg.png differ diff --git a/desktop/icon.png b/desktop/icon.png index f0a50e5aaeae..f837c238d19d 100644 Binary files a/desktop/icon.png and b/desktop/icon.png differ diff --git a/desktop/main.js b/desktop/main.js index 78c13e4d448c..caa4f720a4cf 100644 --- a/desktop/main.js +++ b/desktop/main.js @@ -1,5 +1,6 @@ const { app, + dialog, BrowserWindow, Menu, MenuItem, @@ -80,6 +81,55 @@ const quitAndInstallWithUpdate = () => { autoUpdater.quitAndInstall(); }; +/** + * Menu Item callback to triggers an update check + * @param {MenuItem} menuItem + * @param {BrowserWindow} browserWindow + */ +const manuallyCheckForUpdates = (menuItem, browserWindow) => { + // Disable item until the check (and download) is complete + // eslint: menu item flags like enabled or visible can be dynamically toggled by mutating the object + // eslint-disable-next-line no-param-reassign + menuItem.enabled = false; + + autoUpdater.checkForUpdates() + .catch(error => ({error})) + .then((result) => { + const downloadPromise = result && result.downloadPromise; + + if (downloadPromise) { + dialog.showMessageBox(browserWindow, { + type: 'info', + message: 'Update Available', + detail: 'The new version will be available shortly. We’ll notify you when we’re ready to update.', + buttons: ['Sounds good'], + }); + } else if (result && result.error) { + dialog.showMessageBox(browserWindow, { + type: 'error', + message: 'Update Check Failed', + detail: 'We couldn’t look for an update. Please check again in a bit!', + buttons: ['Okay'], + }); + } else { + dialog.showMessageBox(browserWindow, { + type: 'info', + message: 'Update Not Available', + detail: 'There is no update available as of now! Check again at a later time.', + buttons: ['Okay'], + cancelId: 2, + }); + } + + // By returning the `downloadPromise` we keep "check for updates" disabled if any updates are being downloaded + return downloadPromise; + }) + .finally(() => { + // eslint-disable-next-line no-param-reassign + menuItem.enabled = true; + }); +}; + /** * Trigger event to show keyboard shortcuts * @param {BrowserWindow} browserWindow @@ -91,13 +141,21 @@ const showKeyboardShortcutsModal = (browserWindow) => { browserWindow.webContents.send(ELECTRON_EVENTS.SHOW_KEYBOARD_SHORTCUTS_MODAL); }; -// Defines the system-level menu item for manually triggering an update after +// Defines the system-level menu item to manually apply an update +// This menu item should become visible after an update is downloaded and ready to be applied const updateAppMenuItem = new MenuItem({ label: 'Update New Expensify', - enabled: false, + visible: false, click: quitAndInstallWithUpdate, }); +// System-level menu item to manually check for App updates +const checkForUpdateMenuItem = new MenuItem({ + label: 'Check For Updates', + visible: true, + click: manuallyCheckForUpdates, +}); + // Defines the system-level menu item for opening keyboard shortcuts modal const keyboardShortcutsMenu = new MenuItem({ label: 'View Keyboard Shortcuts', @@ -109,7 +167,8 @@ const electronUpdater = browserWindow => ({ init: () => { autoUpdater.on(ELECTRON_EVENTS.UPDATE_DOWNLOADED, (info) => { downloadedVersion = info.version; - updateAppMenuItem.enabled = true; + updateAppMenuItem.visible = true; + checkForUpdateMenuItem.visible = false; if (browserWindow.isVisible()) { browserWindow.webContents.send(ELECTRON_EVENTS.UPDATE_DOWNLOADED, info.version); } else { @@ -221,7 +280,8 @@ const mainWindow = (() => { const appMenu = _.find(systemMenu.items, item => item.role === 'appmenu'); appMenu.submenu.insert(1, updateAppMenuItem); - appMenu.submenu.insert(2, keyboardShortcutsMenu); + appMenu.submenu.insert(2, checkForUpdateMenuItem); + appMenu.submenu.insert(3, keyboardShortcutsMenu); // On mac, pressing cmd++ actually sends a cmd+=. cmd++ is generally the zoom in shortcut, but this is // not properly listened for by electron. Adding in an invisible cmd+= listener fixes this. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 74060ccb501c..141f59ab8149 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -36,6 +36,29 @@ platform :android do ) end + desc "Build app for testing" + lane :build_internal do + ENV["ENVFILE"]=".env.staging" + + gradle( + project_dir: './android', + task: 'assemble', + build_type: 'InternalRelease', + ) + + aws_s3( + access_key: ENV['S3_ACCESS_KEY'], + secret_access_key: ENV['S3_SECRET_ACCESS_KEY'], + bucket: ENV['S3_BUCKET'], + region: ENV['S3_REGION'], + + apk: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH], + app_directory: "android/#{ENV['PULL_REQUEST_NUMBER']}", + ) + + sh("echo '{\"apk_path\": \"#{lane_context[SharedValues::S3_APK_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../android_paths.json") + end + desc "Build and upload app to Google Play" lane :beta do ENV["ENVFILE"]=".env.production" @@ -91,6 +114,53 @@ platform :ios do ) end + desc "Build app for testing" + lane :build_internal do + require 'securerandom' + ENV["ENVFILE"]=".env.staging" + + keychain_password = SecureRandom.uuid + + create_keychain( + name: "ios-build.keychain", + password: keychain_password, + default_keychain: "true", + unlock: "true", + timeout: "3600", + add_to_search_list: "true" + ) + + import_certificate( + certificate_path: "./ios/Certificates.p12", + keychain_name: "ios-build.keychain", + keychain_password: keychain_password + ) + + install_provisioning_profile( + path: "./ios/chat_expensify_adhoc.mobileprovision" + ) + + build_app( + workspace: "./ios/NewExpensify.xcworkspace", + scheme: "NewExpensify", + export_options: { + manageAppVersionAndBuildNumber: false + } + ) + + aws_s3( + access_key: ENV['S3_ACCESS_KEY'], + secret_access_key: ENV['S3_SECRET_ACCESS_KEY'], + bucket: ENV['S3_BUCKET'], + region: ENV['S3_REGION'], + + ipa: lane_context[SharedValues::IPA_OUTPUT_PATH], + app_directory: "android/#{ENV['PULL_REQUEST_NUMBER']}", + ) + + sh("echo '{\"ipa_path\": \"#{lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../ios_paths.json") + end + desc "Build and upload app to TestFlight" lane :beta do require 'securerandom' diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 000000000000..c6ab0dfb46a4 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-aws_s3' diff --git a/ios/AirshipConfig.plist b/ios/AirshipConfig.plist index 3502dc33584f..32926a3c6bcf 100644 --- a/ios/AirshipConfig.plist +++ b/ios/AirshipConfig.plist @@ -4,13 +4,13 @@ detectProvisioningMode - developmentAppKey + appKey uulSSfTDQJ2r0PMpjRrhmQ - developmentAppSecret + appSecret D4Bhf0HrQEehrPua74Tyiw - productionAppKey + appKey 55vypj0ARc6cN09MX7ogtQ - productionAppSecret + appSecret EsSaqbdLSvmyC6kSBFJCtQ diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 953179f1e1f8..2b3119b554f4 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -766,7 +766,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -791,6 +791,7 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; }; name = Debug; @@ -827,7 +828,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -844,6 +845,7 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; diff --git a/ios/NewExpensify.xcworkspace/contents.xcworkspacedata b/ios/NewExpensify.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..bf66f1bed435 --- /dev/null +++ b/ios/NewExpensify.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/NewExpensify/BootSplash.storyboard b/ios/NewExpensify/BootSplash.storyboard index 8ee04cf74b3b..00b73194f144 100644 --- a/ios/NewExpensify/BootSplash.storyboard +++ b/ios/NewExpensify/BootSplash.storyboard @@ -24,7 +24,7 @@ - + diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/Store.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/Store.png index f9cb090957f4..35d8a1c3cef2 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/Store.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/Store.png differ diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@2x.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@2x.png index 4f10387caf28..2f88e782bfd9 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@2x.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@2x.png differ diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@3x.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@3x.png index 95054fc13d36..cae156c0409f 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@3x.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iOS@3x.png differ diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad.png index 040a3fff300b..c4f9c3c467d0 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad.png differ diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad@2x.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad@2x.png index 7b0abef8d05c..f36fe1698c60 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad@2x.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPad@2x.png differ diff --git a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPadPro.png b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPadPro.png index eedfcdb2e04a..389e9bf64676 100644 Binary files a/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPadPro.png and b/ios/NewExpensify/Images.xcassets/AppIcon.appiconset/iPadPro.png differ diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png index 0aa07e4792b1..7d0644eed692 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo.png differ diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png index b9fc4eb21a78..90bb9b0c4eed 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@2x.png differ diff --git a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png index 59e4967cf66f..295d9fed4b29 100644 Binary files a/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png and b/ios/NewExpensify/Images.xcassets/BootSplashLogo.imageset/bootsplash_logo@3x.png differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 31401f256b65..5d0c8b54ee6f 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.2.29 + 1.2.33 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.2.29.7 + 1.2.33.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0648c76aee1b..c5ec022ebafc 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.2.29 + 1.2.33 CFBundleSignature ???? CFBundleVersion - 1.2.29.7 + 1.2.33.1 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 01ae1cd24420..00b569a90e48 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -17,14 +17,14 @@ PODS: - boost (1.76.0) - CocoaAsyncSocket (7.6.5) - DoubleConversion (1.1.6) - - FBLazyVector (0.70.4-alpha.1) - - FBReactNativeSpec (0.70.4-alpha.1): + - FBLazyVector (0.70.4-alpha.2) + - FBReactNativeSpec (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTRequired (= 0.70.4-alpha.1) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Core (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) + - RCTRequired (= 0.70.4-alpha.2) + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Core (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) - Firebase/Analytics (8.8.0): - Firebase/Core - Firebase/Core (8.8.0): @@ -249,214 +249,214 @@ PODS: - fmt (~> 6.2.1) - glog - libevent - - RCTRequired (0.70.4-alpha.1) - - RCTTypeSafety (0.70.4-alpha.1): - - FBLazyVector (= 0.70.4-alpha.1) - - RCTRequired (= 0.70.4-alpha.1) - - React-Core (= 0.70.4-alpha.1) - - React (0.70.4-alpha.1): - - React-Core (= 0.70.4-alpha.1) - - React-Core/DevSupport (= 0.70.4-alpha.1) - - React-Core/RCTWebSocket (= 0.70.4-alpha.1) - - React-RCTActionSheet (= 0.70.4-alpha.1) - - React-RCTAnimation (= 0.70.4-alpha.1) - - React-RCTBlob (= 0.70.4-alpha.1) - - React-RCTImage (= 0.70.4-alpha.1) - - React-RCTLinking (= 0.70.4-alpha.1) - - React-RCTNetwork (= 0.70.4-alpha.1) - - React-RCTSettings (= 0.70.4-alpha.1) - - React-RCTText (= 0.70.4-alpha.1) - - React-RCTVibration (= 0.70.4-alpha.1) - - React-bridging (0.70.4-alpha.1): + - RCTRequired (0.70.4-alpha.2) + - RCTTypeSafety (0.70.4-alpha.2): + - FBLazyVector (= 0.70.4-alpha.2) + - RCTRequired (= 0.70.4-alpha.2) + - React-Core (= 0.70.4-alpha.2) + - React (0.70.4-alpha.2): + - React-Core (= 0.70.4-alpha.2) + - React-Core/DevSupport (= 0.70.4-alpha.2) + - React-Core/RCTWebSocket (= 0.70.4-alpha.2) + - React-RCTActionSheet (= 0.70.4-alpha.2) + - React-RCTAnimation (= 0.70.4-alpha.2) + - React-RCTBlob (= 0.70.4-alpha.2) + - React-RCTImage (= 0.70.4-alpha.2) + - React-RCTLinking (= 0.70.4-alpha.2) + - React-RCTNetwork (= 0.70.4-alpha.2) + - React-RCTSettings (= 0.70.4-alpha.2) + - React-RCTText (= 0.70.4-alpha.2) + - React-RCTVibration (= 0.70.4-alpha.2) + - React-bridging (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - React-jsi (= 0.70.4-alpha.1) - - React-callinvoker (0.70.4-alpha.1) - - React-Codegen (0.70.4-alpha.1): - - FBReactNativeSpec (= 0.70.4-alpha.1) + - React-jsi (= 0.70.4-alpha.2) + - React-callinvoker (0.70.4-alpha.2) + - React-Codegen (0.70.4-alpha.2): + - FBReactNativeSpec (= 0.70.4-alpha.2) - RCT-Folly (= 2021.07.22.00) - - RCTRequired (= 0.70.4-alpha.1) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Core (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-Core (0.70.4-alpha.1): + - RCTRequired (= 0.70.4-alpha.2) + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Core (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-Core (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.70.4-alpha.1) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-Core/Default (= 0.70.4-alpha.2) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/CoreModulesHeaders (0.70.4-alpha.1): + - React-Core/CoreModulesHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/Default (0.70.4-alpha.1): + - React-Core/Default (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/DevSupport (0.70.4-alpha.1): + - React-Core/DevSupport (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.70.4-alpha.1) - - React-Core/RCTWebSocket (= 0.70.4-alpha.1) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-jsinspector (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-Core/Default (= 0.70.4-alpha.2) + - React-Core/RCTWebSocket (= 0.70.4-alpha.2) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-jsinspector (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTActionSheetHeaders (0.70.4-alpha.1): + - React-Core/RCTActionSheetHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTAnimationHeaders (0.70.4-alpha.1): + - React-Core/RCTAnimationHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTBlobHeaders (0.70.4-alpha.1): + - React-Core/RCTBlobHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTImageHeaders (0.70.4-alpha.1): + - React-Core/RCTImageHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTLinkingHeaders (0.70.4-alpha.1): + - React-Core/RCTLinkingHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTNetworkHeaders (0.70.4-alpha.1): + - React-Core/RCTNetworkHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTSettingsHeaders (0.70.4-alpha.1): + - React-Core/RCTSettingsHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTTextHeaders (0.70.4-alpha.1): + - React-Core/RCTTextHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTVibrationHeaders (0.70.4-alpha.1): + - React-Core/RCTVibrationHeaders (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - React-Core/Default - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-Core/RCTWebSocket (0.70.4-alpha.1): + - React-Core/RCTWebSocket (0.70.4-alpha.2): - glog - RCT-Folly (= 2021.07.22.00) - - React-Core/Default (= 0.70.4-alpha.1) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-Core/Default (= 0.70.4-alpha.2) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - Yoga - - React-CoreModules (0.70.4-alpha.1): + - React-CoreModules (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/CoreModulesHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-RCTImage (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-cxxreact (0.70.4-alpha.1): + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/CoreModulesHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-RCTImage (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-cxxreact (0.70.4-alpha.2): - boost (= 1.76.0) - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-callinvoker (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsinspector (= 0.70.4-alpha.1) - - React-logger (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) - - React-runtimeexecutor (= 0.70.4-alpha.1) - - React-hermes (0.70.4-alpha.1): + - React-callinvoker (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsinspector (= 0.70.4-alpha.2) + - React-logger (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) + - React-runtimeexecutor (= 0.70.4-alpha.2) + - React-hermes (0.70.4-alpha.2): - DoubleConversion - glog - hermes-engine - RCT-Folly (= 2021.07.22.00) - RCT-Folly/Futures (= 2021.07.22.00) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-jsiexecutor (= 0.70.4-alpha.1) - - React-jsinspector (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) - - React-jsi (0.70.4-alpha.1): + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-jsiexecutor (= 0.70.4-alpha.2) + - React-jsinspector (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) + - React-jsi (0.70.4-alpha.2): - boost (= 1.76.0) - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-jsi/Default (= 0.70.4-alpha.1) - - React-jsi/Default (0.70.4-alpha.1): + - React-jsi/Default (= 0.70.4-alpha.2) + - React-jsi/Default (0.70.4-alpha.2): - boost (= 1.76.0) - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-jsiexecutor (0.70.4-alpha.1): + - React-jsiexecutor (0.70.4-alpha.2): - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) - - React-jsinspector (0.70.4-alpha.1) - - React-logger (0.70.4-alpha.1): + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) + - React-jsinspector (0.70.4-alpha.2) + - React-logger (0.70.4-alpha.2): - glog - react-native-blob-util (0.16.2): - React-Core @@ -495,72 +495,72 @@ PODS: - ReactCommon/turbomodule/core - react-native-webview (11.23.0): - React-Core - - React-perflogger (0.70.4-alpha.1) - - React-RCTActionSheet (0.70.4-alpha.1): - - React-Core/RCTActionSheetHeaders (= 0.70.4-alpha.1) - - React-RCTAnimation (0.70.4-alpha.1): + - React-perflogger (0.70.4-alpha.2) + - React-RCTActionSheet (0.70.4-alpha.2): + - React-Core/RCTActionSheetHeaders (= 0.70.4-alpha.2) + - React-RCTAnimation (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTAnimationHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTBlob (0.70.4-alpha.1): + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTAnimationHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTBlob (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTBlobHeaders (= 0.70.4-alpha.1) - - React-Core/RCTWebSocket (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-RCTNetwork (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTImage (0.70.4-alpha.1): + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTBlobHeaders (= 0.70.4-alpha.2) + - React-Core/RCTWebSocket (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-RCTNetwork (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTImage (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTImageHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-RCTNetwork (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTLinking (0.70.4-alpha.1): - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTLinkingHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTNetwork (0.70.4-alpha.1): + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTImageHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-RCTNetwork (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTLinking (0.70.4-alpha.2): + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTLinkingHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTNetwork (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTNetworkHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTSettings (0.70.4-alpha.1): + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTNetworkHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTSettings (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - RCTTypeSafety (= 0.70.4-alpha.1) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTSettingsHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-RCTText (0.70.4-alpha.1): - - React-Core/RCTTextHeaders (= 0.70.4-alpha.1) - - React-RCTVibration (0.70.4-alpha.1): + - RCTTypeSafety (= 0.70.4-alpha.2) + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTSettingsHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-RCTText (0.70.4-alpha.2): + - React-Core/RCTTextHeaders (= 0.70.4-alpha.2) + - React-RCTVibration (0.70.4-alpha.2): - RCT-Folly (= 2021.07.22.00) - - React-Codegen (= 0.70.4-alpha.1) - - React-Core/RCTVibrationHeaders (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (= 0.70.4-alpha.1) - - React-runtimeexecutor (0.70.4-alpha.1): - - React-jsi (= 0.70.4-alpha.1) - - ReactCommon/turbomodule/core (0.70.4-alpha.1): + - React-Codegen (= 0.70.4-alpha.2) + - React-Core/RCTVibrationHeaders (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (= 0.70.4-alpha.2) + - React-runtimeexecutor (0.70.4-alpha.2): + - React-jsi (= 0.70.4-alpha.2) + - ReactCommon/turbomodule/core (0.70.4-alpha.2): - DoubleConversion - glog - RCT-Folly (= 2021.07.22.00) - - React-bridging (= 0.70.4-alpha.1) - - React-callinvoker (= 0.70.4-alpha.1) - - React-Core (= 0.70.4-alpha.1) - - React-cxxreact (= 0.70.4-alpha.1) - - React-jsi (= 0.70.4-alpha.1) - - React-logger (= 0.70.4-alpha.1) - - React-perflogger (= 0.70.4-alpha.1) + - React-bridging (= 0.70.4-alpha.2) + - React-callinvoker (= 0.70.4-alpha.2) + - React-Core (= 0.70.4-alpha.2) + - React-cxxreact (= 0.70.4-alpha.2) + - React-jsi (= 0.70.4-alpha.2) + - React-logger (= 0.70.4-alpha.2) + - React-perflogger (= 0.70.4-alpha.2) - RNCAsyncStorage (1.17.10): - React-Core - RNCClipboard (1.5.1): @@ -594,7 +594,7 @@ PODS: - React-Core - RNReactNativeHapticFeedback (1.14.0): - React-Core - - RNReanimated (3.0.0-rc.3): + - RNReanimated (3.0.0-rc.6): - DoubleConversion - FBLazyVector - FBReactNativeSpec @@ -921,8 +921,8 @@ SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 - FBLazyVector: 674d5b03473bdbc879c79c6ad770d17aef384b03 - FBReactNativeSpec: 3d8684f3599fef61928f5226eb41376945a5bc97 + FBLazyVector: 4c04f10d8958a7be74dc063bc0584ff223b89d4a + FBReactNativeSpec: b6ac4163edc4db460a21e2de83c8a9f3acd5c6cc Firebase: 629510f1a9ddb235f3a7c5c8ceb23ba887f0f814 FirebaseABTesting: 10cbce8db9985ae2e3847ea44e9947dd18f94e10 FirebaseAnalytics: 5506ea8b867d8423485a84b4cd612d279f7b0b8a @@ -960,20 +960,20 @@ SPEC CHECKSUMS: Plaid: 6beadc0828cfd5396c5905931b9503493bbc139a PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda - RCTRequired: 9171c759923bb34cebba3142a996ff0055a26558 - RCTTypeSafety: b6e02adf80bbbb3df7ff287959df499dab9d66b0 - React: a75e21ae0e5409626c31321359785e7e3d2fa4ab - React-bridging: 99c78cffc6d53177130c27c309ffd3ca4a008add - React-callinvoker: 470766a4e2f49490da6e25923e9bd7f19a449cda - React-Codegen: 6f97b1b03090f65582ddccb0b75ea1662f34b54b - React-Core: f780c93e3b8248ca22f08149b1f4f94f85ab24c7 - React-CoreModules: 87351a3490e6bbc8bec65cd5c5c2f5a16a18e01f - React-cxxreact: 1eb773153ae7609bd7ee42c708951742d8cac3c8 - React-hermes: 0f83c702505261ab33e1938ca4a6def638a003e2 - React-jsi: 3fe2db643751becdf3511b77dcb84b68a04b52c4 - React-jsiexecutor: f7a04e9374dbfa320089064f4e6f2c9e10d0740a - React-jsinspector: 0f08037401626246e5c760871a023769b9815619 - React-logger: 0a8e62c840f534936c2cb2c96d0e6d507010e862 + RCTRequired: 329ead02b8edd20fb186d17745a9cadd5ce2922d + RCTTypeSafety: 698418021f8b47d82c058f3115c0026d1874a3ef + React: efefef133cf808a2d6edd1feedf2163d4d7a2922 + React-bridging: c12b25793db0e04c377055b4ddc9db6641be0536 + React-callinvoker: fbb9bbfe86f48efb712149d6c83ec74533e45095 + React-Codegen: 3398733e53c4106cc6fc7602c159669b5fbc1b14 + React-Core: 750c6a39b95c0b427b7624ff48cf1482f05ecf3f + React-CoreModules: 64bec7f728f87706c6512c2e8ca8cdb4f6f41ee3 + React-cxxreact: b515bc7e8b44b4c76ec7f030b00cb11c95692fa0 + React-hermes: 2c7c36834cdbd32b959333fbc17794209f18ecb0 + React-jsi: 9ae06bc7d17e61cee886e985e69f8fa3ef39334a + React-jsiexecutor: 523e742f144a346f7df32dea4fe1944d39b02473 + React-jsinspector: 7b02a02b9b9bf830102394039c80b3bf2e0200b0 + React-logger: b2c47bb0829c3c45ba3a04add9a2b4aea3f3dd4e react-native-blob-util: c3b0faecd2919db568e9d552084396f3e50b57c7 react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866 react-native-config: 7cd105e71d903104e8919261480858940a6b9c0e @@ -989,18 +989,18 @@ SPEC CHECKSUMS: react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 99b24a0c5acd0d5dcac2b1a7f18c49ea317be99a react-native-webview: e771bc375f789ebfa02a26939a57dbc6fa897336 - React-perflogger: d4c9c4cfcc30a0d6cca35ddbd40a85ad4d3e4db1 - React-RCTActionSheet: fe551971745a6b04447a2c04f3d2b26fa5e3728f - React-RCTAnimation: 3a02fced1ff92012a71d4304769c2d430c7a9866 - React-RCTBlob: 9269e1c1fd754d7e8bb8c8a1d0178dc678a9f32f - React-RCTImage: 80c98b6b77063ebe851692000f54fbb9448d8e27 - React-RCTLinking: 7209ca06d58b04ad68148eeaa704f179e05f114b - React-RCTNetwork: 3ef476f0a3d43b7db36217a6f1e197517e6b750b - React-RCTSettings: 9e3dc3833f246062e91a70fed535ccb8c9f6d8c9 - React-RCTText: 89a452bfdb56e0aec4428d083626582a20d07910 - React-RCTVibration: b49dd0970d37e18c716228fce87936b578b7390c - React-runtimeexecutor: a0bd76a1162262d2945d467f6b978116e740904f - ReactCommon: cf8ef38ad293cc8a30b02f858bd16719b64a985b + React-perflogger: b231fb9993c1b5f74e32f0a3473225f9ebaf73d3 + React-RCTActionSheet: 9064b3b78118a253f2d6a63153f6dfff0b38f19f + React-RCTAnimation: d93c521d4ad6457fc8c1d9830ffb0495286ea036 + React-RCTBlob: 3121c9d4416d1d4c8d05a600f7a27da85458a277 + React-RCTImage: fac3e6e809d2e79669cc9deec9e4802dde9d73cd + React-RCTLinking: 23718d841e65fe258dd64e9cc0681d6767f89196 + React-RCTNetwork: 1bf07637ebb27f8241557e85871f7af990238130 + React-RCTSettings: 02f399b023b23a853eeda24879f4703755ffc996 + React-RCTText: c916d8da274c9823aaed9f53c007ecfa6e802764 + React-RCTVibration: eb5fb0bb3d78e31dcaeae5f127c75a7caf4b6af9 + React-runtimeexecutor: a8cee6fe3fb7994f07b735c7024a08a4fa5f8446 + ReactCommon: 1ebf2df5c764ebb52048e7a1cf42c75a6246171a RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 RNCPicker: 0b65be85fe7954fbb2062ef079e3d1cde252d888 @@ -1013,14 +1013,14 @@ SPEC CHECKSUMS: RNGestureHandler: 920eb17f5b1e15dae6e5ed1904045f8f90e0b11e RNPermissions: dcdb7b99796bbeda6975a6e79ad519c41b251b1c RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c - RNReanimated: 6c980139eb3b043569a08b8cb3d92cdf46bd54fa + RNReanimated: 069f3aff5df4cbefaf81589c0622370073a89f1d RNScreens: 0df01424e9e0ed7827200d6ed1087ddd06c493f9 RNSVG: 38ca962c970dbce1ca38991a5aebf26d163f9efb SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 urbanairship-react-native: 7e2e9a84c541b1d04798e51f7f390a2d5806eac0 - Yoga: affc5393470ecec3069bd7bf05d5877afffff995 + Yoga: f77f6497bccebdcbc8efb03dbf83eadfdec6d104 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a PODFILE CHECKSUM: 48093a300788c218e76f70cf6d102915cd993f34 diff --git a/package-lock.json b/package-lock.json index a0987ad01eb1..e57125255b26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.2.29-7", + "version": "1.2.33-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.2.29-7", + "version": "1.2.33-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -38,7 +38,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#dd66d931aeffe18cdcffab8e7d05af32f3ef7ad1", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#87d6599f2fadd3601067ac9cfa5d10681fa34811", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", @@ -56,7 +56,7 @@ "react-collapse": "^5.1.0", "react-content-loader": "^6.1.0", "react-dom": "18.1.0", - "react-native": "npm:@expensify/react-native@0.70.4-alpha.1", + "react-native": "npm:@expensify/react-native@0.70.4-alpha.2", "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", @@ -74,7 +74,7 @@ "react-native-permissions": "^3.0.1", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10", "react-native-plaid-link-sdk": "^7.2.0", - "react-native-reanimated": "3.0.0-rc.3", + "react-native-reanimated": "3.0.0-rc.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", @@ -23861,8 +23861,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#dd66d931aeffe18cdcffab8e7d05af32f3ef7ad1", - "integrity": "sha512-6UVLbHiKHe34NLzD5+jf3WEqA9HGEvmbeIwDPEkAckL1I1+8Y6a9/GonICUg/b4V/O/ZcjUByOv/055awKqz2g==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#87d6599f2fadd3601067ac9cfa5d10681fa34811", + "integrity": "sha512-8Pr8W7cbfxLxaNK8UZm0II1zzml55M6XvACFy8uFKC+vbugP8sH644n+e+VmQ4mgcrEs19rrM/VG4ehxDO4nww==", "license": "MIT", "dependencies": { "classnames": "2.3.1", @@ -35213,9 +35213,9 @@ }, "node_modules/react-native": { "name": "@expensify/react-native", - "version": "0.70.4-alpha.1", - "resolved": "https://registry.npmjs.org/@expensify/react-native/-/react-native-0.70.4-alpha.1.tgz", - "integrity": "sha512-kXVxU+ni4SnJgCAENaS8ri/CwO2lt1Yqu0y0Y9SjCgayk2ZsSXsp13ajYfICOpPfrOLRvMjQE8cAMQuuzDqovg==", + "version": "0.70.4-alpha.2", + "resolved": "https://registry.npmjs.org/@expensify/react-native/-/react-native-0.70.4-alpha.2.tgz", + "integrity": "sha512-JpB56JP1YXrhuV4TQTVGJNppWcrCcfCvkk5hZhOieArfmi7iJsoCF3urLEj88EZmAHrnf7DrQgOYki++hVwg7A==", "dependencies": { "@jest/create-cache-key-function": "^29.0.3", "@react-native-community/cli": "9.2.1", @@ -35539,9 +35539,9 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.0.0-rc.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.0.0-rc.3.tgz", - "integrity": "sha512-kFkbazXfhq/zb3sQyLIFegLqxzPMJAd36pbz7jtOphZ1mEXDadX0ODp6eD7/PJHnLqYGHtsqSI8WMMLLN6rdjw==", + "version": "3.0.0-rc.6", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.0.0-rc.6.tgz", + "integrity": "sha512-XdJP4yESxU5kyxZtExwa4uEBF3fUJ7jQTzptMg/KBANZazKnNUW+UwgpW6MQ0xyLSqcHY/essYcGadCU6wHu3A==", "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -60824,9 +60824,9 @@ } }, "expensify-common": { - "version": "git+ssh://git@github.com/Expensify/expensify-common.git#dd66d931aeffe18cdcffab8e7d05af32f3ef7ad1", - "integrity": "sha512-6UVLbHiKHe34NLzD5+jf3WEqA9HGEvmbeIwDPEkAckL1I1+8Y6a9/GonICUg/b4V/O/ZcjUByOv/055awKqz2g==", - "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#dd66d931aeffe18cdcffab8e7d05af32f3ef7ad1", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#87d6599f2fadd3601067ac9cfa5d10681fa34811", + "integrity": "sha512-8Pr8W7cbfxLxaNK8UZm0II1zzml55M6XvACFy8uFKC+vbugP8sH644n+e+VmQ4mgcrEs19rrM/VG4ehxDO4nww==", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#87d6599f2fadd3601067ac9cfa5d10681fa34811", "requires": { "classnames": "2.3.1", "clipboard": "2.0.4", @@ -69510,9 +69510,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-native": { - "version": "npm:@expensify/react-native@0.70.4-alpha.1", - "resolved": "https://registry.npmjs.org/@expensify/react-native/-/react-native-0.70.4-alpha.1.tgz", - "integrity": "sha512-kXVxU+ni4SnJgCAENaS8ri/CwO2lt1Yqu0y0Y9SjCgayk2ZsSXsp13ajYfICOpPfrOLRvMjQE8cAMQuuzDqovg==", + "version": "npm:@expensify/react-native@0.70.4-alpha.2", + "resolved": "https://registry.npmjs.org/@expensify/react-native/-/react-native-0.70.4-alpha.2.tgz", + "integrity": "sha512-JpB56JP1YXrhuV4TQTVGJNppWcrCcfCvkk5hZhOieArfmi7iJsoCF3urLEj88EZmAHrnf7DrQgOYki++hVwg7A==", "requires": { "@jest/create-cache-key-function": "^29.0.3", "@react-native-community/cli": "9.2.1", @@ -69853,9 +69853,9 @@ "requires": {} }, "react-native-reanimated": { - "version": "3.0.0-rc.3", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.0.0-rc.3.tgz", - "integrity": "sha512-kFkbazXfhq/zb3sQyLIFegLqxzPMJAd36pbz7jtOphZ1mEXDadX0ODp6eD7/PJHnLqYGHtsqSI8WMMLLN6rdjw==", + "version": "3.0.0-rc.6", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.0.0-rc.6.tgz", + "integrity": "sha512-XdJP4yESxU5kyxZtExwa4uEBF3fUJ7jQTzptMg/KBANZazKnNUW+UwgpW6MQ0xyLSqcHY/essYcGadCU6wHu3A==", "requires": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", diff --git a/package.json b/package.json index 9a26730974b1..c7643887143d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.2.29-7", + "version": "1.2.33-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.", @@ -22,6 +22,7 @@ "desktop": "scripts/set-pusher-suffix.sh && node desktop/start.js", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", + "desktop-build-internal": "scripts/build-desktop.sh internal", "ios-build": "fastlane ios build", "android-build": "fastlane android build", "android-build-e2e": "bundle exec fastlane android build_e2e", @@ -67,7 +68,7 @@ "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", "dotenv": "^8.2.0", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#dd66d931aeffe18cdcffab8e7d05af32f3ef7ad1", + "expensify-common": "git+https://github.com/Expensify/expensify-common.git#87d6599f2fadd3601067ac9cfa5d10681fa34811", "fbjs": "^3.0.2", "file-loader": "^6.0.0", "html-entities": "^1.3.1", @@ -85,7 +86,7 @@ "react-collapse": "^5.1.0", "react-content-loader": "^6.1.0", "react-dom": "18.1.0", - "react-native": "npm:@expensify/react-native@0.70.4-alpha.1", + "react-native": "npm:@expensify/react-native@0.70.4-alpha.2", "react-native-blob-util": "^0.16.2", "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", @@ -103,7 +104,7 @@ "react-native-permissions": "^3.0.1", "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#7f09b2c15ffae320d769788f75bdf8948714bb10", "react-native-plaid-link-sdk": "^7.2.0", - "react-native-reanimated": "3.0.0-rc.3", + "react-native-reanimated": "3.0.0-rc.6", "react-native-render-html": "6.3.1", "react-native-safe-area-context": "4.4.1", "react-native-screens": "3.17.0", diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch new file mode 100644 index 000000000000..576858f1f5f7 --- /dev/null +++ b/patches/react-native-modal+13.0.1.patch @@ -0,0 +1,46 @@ +diff --git a/node_modules/react-native-modal/dist/modal.d.ts b/node_modules/react-native-modal/dist/modal.d.ts +index b63bcfc..bd6419e 100644 +--- a/node_modules/react-native-modal/dist/modal.d.ts ++++ b/node_modules/react-native-modal/dist/modal.d.ts +@@ -161,6 +161,7 @@ export declare class ReactNativeModal extends React.Component + getDeviceHeight: () => number; + getDeviceWidth: () => number; + onBackButtonPress: () => boolean; ++ handleEscape: (e: KeyboardEvent) => void; + shouldPropagateSwipe: (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => boolean; + 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..fe028ab 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 { + } + return false; + }; ++ this.handleEscape = (e) => { ++ if (e.key === 'Escape') { ++ if (this.onBackButtonPress() === true) { ++ e.stopImmediatePropagation(); ++ } ++ } ++ }; + 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 { + this.open(); + } + BackHandler.addEventListener('hardwareBackPress', this.onBackButtonPress); ++ if (Platform.OS === 'web') { ++ document?.body?.addEventListener?.('keyup', this.handleEscape, true); ++ } + } + componentWillUnmount() { + BackHandler.removeEventListener('hardwareBackPress', this.onBackButtonPress); ++ if (Platform.OS === 'web') { ++ document?.body?.removeEventListener?.('keyup', this.handleEscape, true); ++ } + if (this.didUpdateDimensionsEmitter) { + this.didUpdateDimensionsEmitter.remove(); + } diff --git a/patches/react-native-reanimated+3.0.0-rc.3.patch b/patches/react-native-reanimated+3.0.0-rc.3.patch deleted file mode 100644 index 321bcc07419f..000000000000 --- a/patches/react-native-reanimated+3.0.0-rc.3.patch +++ /dev/null @@ -1,64 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/RNReanimated.podspec b/node_modules/react-native-reanimated/RNReanimated.podspec -index 3205c93..c7f2821 100644 ---- a/node_modules/react-native-reanimated/RNReanimated.podspec -+++ b/node_modules/react-native-reanimated/RNReanimated.podspec -@@ -49,7 +49,12 @@ rescue - end - end - --reactCommonDir = File.join(nodeModulesDir, "react-native", "ReactCommon") -+ -+# From: https://github.com/software-mansion/react-native-reanimated/pull/3701/files -+pods_root = Pod::Config.instance.project_pods_root -+react_native_common_dir_absolute = File.join(nodeModulesDir, 'react-native', 'ReactCommon') -+react_native_common_dir_relative = Pathname.new(react_native_common_dir_absolute).relative_path_from(pods_root).to_s -+reactCommonDir = react_native_common_dir_relative - - if isUserApp - libInstances = %x[find ../../ -name "package.json" | grep "/react-native-reanimated/package.json" | grep -v "/.yarn/"] -@@ -111,9 +116,10 @@ Pod::Spec.new do |s| - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", - } - s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags + ' -DHERMES_ENABLE_DEBUGGER' -- s.xcconfig = { -- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/glog\" \"$(PODS_ROOT)/#{folly_prefix}Folly\" \"$(PODS_ROOT)/RCT-Folly\" \"${PODS_ROOT}/Headers/Public/React-hermes\" \"${PODS_ROOT}/Headers/Public/hermes-engine\" \"#{reactCommonDir}\"", -- "OTHER_CFLAGS" => "$(inherited)" + " " + folly_flags + " " + fabric_flags } -+ s.xcconfig = { -+ "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/glog\" \"$(PODS_ROOT)/#{folly_prefix}Folly\" \"$(PODS_ROOT)/Headers/Public/React-hermes\" \"$(PODS_ROOT)/Headers/Public/hermes-engine\" \"$(PODS_ROOT)/#{reactCommonDir}\"", -+ "OTHER_CFLAGS" => "$(inherited)" + " " + folly_flags + " " + fabric_flags -+ } - - s.requires_arc = true - -diff --git a/node_modules/react-native-reanimated/ios/REAModule.mm b/node_modules/react-native-reanimated/ios/REAModule.mm -index e1cd2f4..8ba248d 100644 ---- a/node_modules/react-native-reanimated/ios/REAModule.mm -+++ b/node_modules/react-native-reanimated/ios/REAModule.mm -@@ -52,6 +52,7 @@ @implementation REAModule { - #ifdef DEBUG - SingleInstanceChecker singleInstanceChecker_; - #endif -+ bool hasListeners; - } - - RCT_EXPORT_MODULE(ReanimatedModule); -@@ -291,4 +292,19 @@ - (void)eventDispatcherWillDispatchEvent:(id)event - [_nodesManager dispatchEvent:event]; - } - -+- (void)startObserving { -+ hasListeners = YES; -+} -+ -+- (void)stopObserving { -+ hasListeners = NO; -+} -+ -+- (void)sendEventWithName:(NSString *)eventName body:(id)body -+{ -+ if (hasListeners) { -+ [super sendEventWithName:eventName body:body]; -+ } -+} -+ - @end diff --git a/scripts/build-desktop.sh b/scripts/build-desktop.sh index 3e5024fc758c..ce8737ee1b18 100755 --- a/scripts/build-desktop.sh +++ b/scripts/build-desktop.sh @@ -5,6 +5,8 @@ export ELECTRON_ENV=${1:-development} if [[ "$ELECTRON_ENV" == "staging" ]]; then ENV_FILE=".env.staging" +elif [[ "$ELECTRON_ENV" == "internal" ]]; then + ENV_FILE=".env.staging" elif [[ "$ELECTRON_ENV" == "production" ]]; then ENV_FILE=".env.production" else diff --git a/src/CONFIG.js b/src/CONFIG.js index 57260b676e79..9b7648916da0 100644 --- a/src/CONFIG.js +++ b/src/CONFIG.js @@ -1,10 +1,14 @@ -import lodashGet from 'lodash/get'; +import get from 'lodash/get'; import {Platform} from 'react-native'; import Config from 'react-native-config'; import getPlatform from './libs/getPlatform/index'; import * as Url from './libs/Url'; import CONST from './CONST'; +// react-native-config doesn't trim whitespace on iOS for some reason so we +// add a trim() call to lodashGet here to prevent headaches +const lodashGet = (config, key, defaultValue) => get(config, key, defaultValue).trim(); + // Set default values to contributor friendly values to make development work out of the box without an .env file const ENVIRONMENT = lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV); const newExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/')); diff --git a/src/CONST.js b/src/CONST.js index fb0bd7ba6a2b..833985247776 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -40,6 +40,7 @@ const CONST = { }, DATE: { MOMENT_FORMAT_STRING: 'YYYY-MM-DD', + UNIX_EPOCH: '1970-01-01 00:00:00.000', }, SMS: { DOMAIN: '@expensify.sms', @@ -104,7 +105,11 @@ const CONST = { }, MAX_LENGTH: { SSN: 4, - ZIP_CODE: 5, + ZIP_CODE: 10, + }, + TYPE: { + BUSINESS: 'BUSINESS', + PERSONAL: 'PERSONAL', }, }, INCORPORATION_TYPES: { @@ -226,6 +231,7 @@ const CONST = { DEEPLINK_BASE_URL: 'new-expensify://', PDF_VIEWER_URL: '/pdf/web/viewer.html', EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`, + CONCIERGE_ICON_URL: `${CLOUDFRONT_URL}/images/icons/concierge_2022.png`, UPWORK_URL: 'https://github.com/Expensify/App/issues?q=is%3Aopen+is%3Aissue+label%3A%22Help+Wanted%22', GITHUB_URL: 'https://github.com/Expensify/App', TERMS_URL: `${USE_EXPENSIFY_URL}/terms`, @@ -685,6 +691,8 @@ const CONST = { }, ROLE: { ADMIN: 'admin', + AUDITOR: 'auditor', + USER: 'user', }, ROOM_PREFIX: '#', CUSTOM_UNIT_RATE_BASE_OFFSET: 100, diff --git a/src/components/Avatar.js b/src/components/Avatar.js index aa4f6dbf92fd..05caa431c970 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -9,6 +9,7 @@ import CONST from '../CONST'; import * as StyleUtils from '../styles/StyleUtils'; import * as Expensicons from './Icon/Expensicons'; import getAvatarDefaultSource from '../libs/getAvatarDefaultSource'; +import styles from '../styles/styles'; const propTypes = { /** Source for the avatar. Can be a URL or an icon. */ @@ -58,18 +59,25 @@ class Avatar extends PureComponent { ...this.props.imageStyles, ]; + const iconStyle = [ + StyleUtils.getAvatarStyle(this.props.size), + styles.bgTransparent, + ...this.props.imageStyles, + ]; const iconSize = StyleUtils.getAvatarSize(this.props.size); return ( {_.isFunction(this.props.source) || this.state.imageError ? ( - + + + ) : ( - + {this.props.isChecked && } )} diff --git a/src/components/CustomStatusBar/index.android.js b/src/components/CustomStatusBar/index.android.js index 464d7437392d..fa3f8921e82a 100644 --- a/src/components/CustomStatusBar/index.android.js +++ b/src/components/CustomStatusBar/index.android.js @@ -1,5 +1,6 @@ import React from 'react'; import {StatusBar} from 'react-native'; +import themeColors from '../../styles/themes/default'; /** * Only the Android platform supports "setBackgroundColor" @@ -7,9 +8,8 @@ import {StatusBar} from 'react-native'; export default class CustomStatusBar extends React.Component { componentDidMount() { - StatusBar.setBarStyle('dark-content'); - StatusBar.setBackgroundColor('transparent'); - StatusBar.setTranslucent(true); + StatusBar.setBarStyle('light-content'); + StatusBar.setBackgroundColor(themeColors.appBG); } render() { diff --git a/src/components/CustomStatusBar/index.js b/src/components/CustomStatusBar/index.js index 0c5f999365cb..e0f344e9e1c0 100644 --- a/src/components/CustomStatusBar/index.js +++ b/src/components/CustomStatusBar/index.js @@ -3,7 +3,7 @@ import {StatusBar} from 'react-native'; export default class CustomStatusBar extends React.Component { componentDidMount() { - StatusBar.setBarStyle('dark-content', true); + StatusBar.setBarStyle('light-content', true); } render() { diff --git a/src/components/Form.js b/src/components/Form.js index 74f3bbf12fed..4734381bc935 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -144,7 +144,11 @@ class Form extends React.Component { const errors = _.pick(validationErrors, (inputValue, inputID) => ( Boolean(this.touchedInputs[inputID]) )); - this.setState({errors}); + + if (!_.isEqual(errors, this.state.errors)) { + this.setState({errors}); + } + return errors; } diff --git a/src/components/Icon/Illustrations.js b/src/components/Icon/Illustrations.js index 273f5854a3d7..20c2006a3b82 100644 --- a/src/components/Icon/Illustrations.js +++ b/src/components/Icon/Illustrations.js @@ -17,6 +17,22 @@ import RocketOrange from '../../../assets/images/product-illustrations/rocket--o import TadaYellow from '../../../assets/images/product-illustrations/tada--yellow.svg'; import TadaBlue from '../../../assets/images/product-illustrations/tada--blue.svg'; import GpsTrackOrange from '../../../assets/images/product-illustrations/gps-track--orange.svg'; +import MoneyReceipts from '../../../assets/images/simple-illustrations/simple-illustration__money-receipts.svg'; +import PinkBill from '../../../assets/images/simple-illustrations/simple-illustration__bill.svg'; +import CreditCardsNew from '../../../assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; +import InvoiceBlue from '../../../assets/images/simple-illustrations/simple-illustration__invoice.svg'; +import LockOpen from '../../../assets/images/simple-illustrations/simple-illustration__lockopen.svg'; +import Luggage from '../../../assets/images/simple-illustrations/simple-illustration__luggage.svg'; +import MoneyIntoWallet from '../../../assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg'; +import MoneyWings from '../../../assets/images/simple-illustrations/simple-illustration__moneywings.svg'; +import OpenSafe from '../../../assets/images/simple-illustrations/simple-illustration__opensafe.svg'; +import TrackShoe from '../../../assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; +import BankArrow from '../../../assets/images/simple-illustrations/simple-illustration__bank-arrow.svg'; +import ConciergeBubble from '../../../assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; +import ConciergeNew from '../../../assets/images/simple-illustrations/simple-illustration__concierge.svg'; +import MoneyBadge from '../../../assets/images/simple-illustrations/simple-illustration__moneybadge.svg'; +import TreasureChest from '../../../assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; +import ThumbsUpStars from '../../../assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; export { BankArrowPink, @@ -38,4 +54,20 @@ export { TadaYellow, TadaBlue, GpsTrackOrange, + MoneyReceipts, + PinkBill, + CreditCardsNew, + InvoiceBlue, + LockOpen, + Luggage, + MoneyIntoWallet, + MoneyWings, + OpenSafe, + TrackShoe, + BankArrow, + ConciergeBubble, + ConciergeNew, + MoneyBadge, + TreasureChest, + ThumbsUpStars, }; diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js index e3b0a6ef0e07..71380ac19d78 100644 --- a/src/components/ImageView/index.native.js +++ b/src/components/ImageView/index.native.js @@ -74,11 +74,8 @@ class ImageView extends PureComponent { const aspectRatio = Math.min(containerHeight / imageHeight, containerWidth / imageWidth); - if (imageHeight > imageWidth) { - imageHeight *= aspectRatio; - } else { - imageWidth *= aspectRatio; - } + imageHeight *= aspectRatio; + imageWidth *= aspectRatio; // Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well. const maxDimensionsScale = 11; diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 71028c7a760b..f8ea7b5d9ed2 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -134,7 +134,6 @@ class BaseInvertedFlatList extends Component { // eslint-disable-next-line react/jsx-props-no-spreading {...this.props} ref={this.props.innerRef} - inverted renderItem={this.renderItem} sizeMap={this.sizeMap} diff --git a/src/components/InvertedFlatList/index.android.js b/src/components/InvertedFlatList/index.android.js new file mode 100644 index 000000000000..779e430f94d8 --- /dev/null +++ b/src/components/InvertedFlatList/index.android.js @@ -0,0 +1,24 @@ +import React, {forwardRef} from 'react'; +import {View} from 'react-native'; +import BaseInvertedFlatList from './BaseInvertedFlatList'; +import styles from '../../styles/styles'; + +const InvertedCell = props => ( + // eslint-disable-next-line react/jsx-props-no-spreading + +); + +export default forwardRef((props, ref) => ( + +)); diff --git a/src/components/InvertedFlatList/index.native.js b/src/components/InvertedFlatList/index.ios.js similarity index 94% rename from src/components/InvertedFlatList/index.native.js rename to src/components/InvertedFlatList/index.ios.js index e34d2d260289..d502162fe7bb 100644 --- a/src/components/InvertedFlatList/index.native.js +++ b/src/components/InvertedFlatList/index.ios.js @@ -6,5 +6,6 @@ export default forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} + inverted /> )); diff --git a/src/components/InvertedFlatList/index.js b/src/components/InvertedFlatList/index.js index a6a2dc688fc5..55efffed6293 100644 --- a/src/components/InvertedFlatList/index.js +++ b/src/components/InvertedFlatList/index.js @@ -41,6 +41,7 @@ class InvertedFlatList extends React.Component { this.list = el} shouldMeasureItems contentContainerStyle={StyleSheet.compose(this.props.contentContainerStyle, styles.justifyContentEnd)} diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.js index 92c868548001..5b8aaf20223f 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.js @@ -33,7 +33,7 @@ class Onfido extends React.Component { colorContentTitle: themeColors.text, colorContentSubtitle: themeColors.text, colorContentBody: themeColors.text, - borderRadiusButton: `${variables.componentBorderRadius}px`, + borderRadiusButton: `${variables.buttonBorderRadius}px`, colorBackgroundSurfaceModal: themeColors.appBG, colorBorderDocTypeButton: themeColors.border, colorBorderDocTypeButtonHover: themeColors.link, diff --git a/src/components/ReportActionItem/IOUAction.js b/src/components/ReportActionItem/IOUAction.js index 015766612056..71717dad1faf 100644 --- a/src/components/ReportActionItem/IOUAction.js +++ b/src/components/ReportActionItem/IOUAction.js @@ -26,12 +26,17 @@ const propTypes = { /** The participants of this report */ participants: PropTypes.arrayOf(PropTypes.string), }), + + /** Whether the IOU is hovered so we can modify its style */ + isHovered: PropTypes.bool, + }; const defaultProps = { chatReport: { participants: [], }, + isHovered: false, }; const IOUAction = (props) => { @@ -42,7 +47,7 @@ const IOUAction = (props) => { <> {((props.isMostRecentIOUReportAction && Boolean(props.action.originalMessage.IOUReportID)) @@ -53,7 +58,13 @@ const IOUAction = (props) => { chatReportID={props.chatReportID} onPayButtonPressed={launchDetailsModal} onPreviewPressed={launchDetailsModal} - containerStyles={[styles.cursorPointer]} + containerStyles={[ + styles.cursorPointer, + props.isHovered + ? styles.iouPreviewBoxHover + : undefined, + ]} + isHovered={props.isHovered} /> )} diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js index 355c414e48aa..cee2eaa60c6e 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/IOUPreview.js @@ -65,6 +65,9 @@ const propTypes = { hasOutstandingIOU: PropTypes.bool, }), + /** True if the IOU Preview card is hovered */ + isHovered: PropTypes.bool, + /** All of the personal details for everyone */ personalDetails: PropTypes.objectOf(PropTypes.shape({ @@ -95,6 +98,7 @@ const defaultProps = { containerStyles: [], walletTerms: {}, pendingAction: null, + isHovered: false, }; const IOUPreview = (props) => { @@ -159,7 +163,12 @@ const IOUPreview = (props) => { @@ -183,12 +192,12 @@ const IOUPreview = (props) => { && !props.shouldHidePayButton && props.iouReport.stateNum === CONST.REPORT.STATE_NUM.PROCESSING && ( diff --git a/src/components/ReportActionItem/IOUQuote.js b/src/components/ReportActionItem/IOUQuote.js index 5f991c8ecf05..364bb089c13f 100644 --- a/src/components/ReportActionItem/IOUQuote.js +++ b/src/components/ReportActionItem/IOUQuote.js @@ -1,10 +1,13 @@ import React from 'react'; -import {View} from 'react-native'; +import {View, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import _ from 'underscore'; import Str from 'expensify-common/lib/str'; import Text from '../Text'; +import Icon from '../Icon'; +import * as Expensicons from '../Icon/Expensicons'; import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; import withLocalize, {withLocalizePropTypes} from '../withLocalize'; @@ -12,8 +15,8 @@ const propTypes = { /** All the data of the action */ action: PropTypes.shape(reportActionPropTypes).isRequired, - /** Should the View Details link be displayed? */ - shouldShowViewDetailsLink: PropTypes.bool, + /** Whether it is allowed to view details. */ + shouldAllowViewDetails: PropTypes.bool, /** Callback invoked when View Details is pressed */ onViewDetailsPressed: PropTypes.func, @@ -22,26 +25,37 @@ const propTypes = { }; const defaultProps = { - shouldShowViewDetailsLink: false, + shouldAllowViewDetails: false, onViewDetailsPressed: () => {}, }; const IOUQuote = props => ( {_.map(props.action.message, (fragment, index) => ( - - - {Str.htmlDecode(fragment.text)} - - {props.shouldShowViewDetailsLink && ( - - {props.translate('iou.viewDetails')} + {}} + style={[styles.flexRow, styles.justifyContentBetween, + props.shouldAllowViewDetails + ? undefined + : styles.cursorDefault, + ]} + focusable={props.shouldAllowViewDetails} + > + + + {/* Get first word of IOU message */} + {Str.htmlDecode(fragment.text.split(' ')[0])} + + + {/* Get remainder of IOU message */} + {Str.htmlDecode(fragment.text.substring(fragment.text.indexOf(' ')))} - )} - + + + ))} ); diff --git a/src/components/ReportActionsSkeletonView/SkeletonViewLines.js b/src/components/ReportActionsSkeletonView/SkeletonViewLines.js index e70fd65cae87..30e96d5784dd 100644 --- a/src/components/ReportActionsSkeletonView/SkeletonViewLines.js +++ b/src/components/ReportActionsSkeletonView/SkeletonViewLines.js @@ -4,21 +4,29 @@ import {Rect, Circle} from 'react-native-svg'; import SkeletonViewContentLoader from 'react-content-loader/native'; import CONST from '../../CONST'; import themeColors from '../../styles/themes/default'; +import styles from '../../styles/styles'; const propTypes = { /** Number of rows to show in Skeleton UI block */ numberOfRows: PropTypes.number.isRequired, + animate: PropTypes.bool, +}; + +const defaultTypes = { + animate: true, }; const SkeletonViewLines = props => ( - + {props.numberOfRows > 1 && } {props.numberOfRows > 2 && } @@ -26,4 +34,5 @@ const SkeletonViewLines = props => ( SkeletonViewLines.displayName = 'SkeletonViewLines'; SkeletonViewLines.propTypes = propTypes; +SkeletonViewLines.defaultProps = defaultTypes; export default SkeletonViewLines; diff --git a/src/components/ReportActionsSkeletonView/index.js b/src/components/ReportActionsSkeletonView/index.js index 1062fd665450..4f7055d98938 100644 --- a/src/components/ReportActionsSkeletonView/index.js +++ b/src/components/ReportActionsSkeletonView/index.js @@ -6,6 +6,13 @@ import CONST from '../../CONST'; const propTypes = { /** Height of the container component */ containerHeight: PropTypes.number.isRequired, + + /** Whether to animate the skeleton view */ + animate: PropTypes.bool, +}; + +const defaultProps = { + animate: true, }; const ReportActionsSkeletonView = (props) => { @@ -16,13 +23,13 @@ const ReportActionsSkeletonView = (props) => { const iconIndex = (index + 1) % 4; switch (iconIndex) { case 2: - skeletonViewLines.push(); + skeletonViewLines.push(); break; case 0: - skeletonViewLines.push(); + skeletonViewLines.push(); break; default: - skeletonViewLines.push(); + skeletonViewLines.push(); } } return <>{skeletonViewLines}; @@ -30,4 +37,5 @@ const ReportActionsSkeletonView = (props) => { ReportActionsSkeletonView.displayName = 'ReportActionsSkeletonView'; ReportActionsSkeletonView.propTypes = propTypes; +ReportActionsSkeletonView.defaultProps = defaultProps; export default ReportActionsSkeletonView; diff --git a/src/components/ReportHeaderSkeletonView.js b/src/components/ReportHeaderSkeletonView.js index fd7818573ffe..6196fca054fd 100644 --- a/src/components/ReportHeaderSkeletonView.js +++ b/src/components/ReportHeaderSkeletonView.js @@ -2,6 +2,7 @@ import React from 'react'; import {Pressable, View} from 'react-native'; import {Rect, Circle} from 'react-native-svg'; import SkeletonViewContentLoader from 'react-content-loader/native'; +import PropTypes from 'prop-types'; import styles from '../styles/styles'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -11,6 +12,11 @@ import themeColors from '../styles/themes/default'; const propTypes = { ...windowDimensionsPropTypes, + animate: PropTypes.bool, +}; + +const defaultProps = { + animate: true, }; const ReportHeaderSkeletonView = props => ( @@ -23,8 +29,9 @@ const ReportHeaderSkeletonView = props => ( @@ -37,5 +44,6 @@ const ReportHeaderSkeletonView = props => ( ); ReportHeaderSkeletonView.propTypes = propTypes; +ReportHeaderSkeletonView.defaultProps = defaultProps; ReportHeaderSkeletonView.displayName = 'ReportHeaderSkeletonView'; export default withWindowDimensions(ReportHeaderSkeletonView); diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js deleted file mode 100644 index 95e016cd6a18..000000000000 --- a/src/components/RoomNameInput.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import CONST from '../CONST'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import TextInput from './TextInput'; - -const propTypes = { - /** Callback to execute when the text input is modified correctly */ - onChangeText: PropTypes.func, - - /** Room name to show in input field. This should include the '#' already prefixed to the name */ - value: PropTypes.string, - - /** Whether we should show the input as disabled */ - disabled: PropTypes.bool, - - /** Error text to show */ - errorText: PropTypes.string, - - ...withLocalizePropTypes, - - /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, -}; - -const defaultProps = { - onChangeText: () => {}, - value: '', - disabled: false, - errorText: '', - forwardedRef: () => {}, -}; - -class RoomNameInput extends Component { - constructor(props) { - super(props); - - this.setModifiedRoomName = this.setModifiedRoomName.bind(this); - } - - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - setModifiedRoomName(event) { - const roomName = event.nativeEvent.text; - const modifiedRoomName = this.modifyRoomName(roomName); - this.props.onChangeText(modifiedRoomName); - } - - /** - * Modifies the room name to follow our conventions: - * - Max length 80 characters - * - Cannot not include space or special characters, and we automatically apply an underscore for spaces - * - Must be lowercase - * @param {String} roomName - * @returns {String} - */ - modifyRoomName(roomName) { - const modifiedRoomNameWithoutHash = roomName - .replace(/ /g, '_') - .replace(/[^a-zA-Z\d_]/g, '') - .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) - .toLowerCase(); - - return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; - } - - render() { - return ( - - ); - } -} - -RoomNameInput.propTypes = propTypes; -RoomNameInput.defaultProps = defaultProps; - -export default withLocalize( - React.forwardRef((props, ref) => ( - // eslint-disable-next-line react/jsx-props-no-spreading - - )), -); diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js new file mode 100644 index 000000000000..6c015ec361de --- /dev/null +++ b/src/components/RoomNameInput/index.js @@ -0,0 +1,81 @@ +import React, {Component} from 'react'; +import CONST from '../../CONST'; +import withLocalize from '../withLocalize'; +import TextInput from '../TextInput'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils'; + +class RoomNameInput extends Component { + constructor(props) { + super(props); + + this.setModifiedRoomName = this.setModifiedRoomName.bind(this); + this.setSelection = this.setSelection.bind(this); + + this.state = { + selection: undefined, + }; + } + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + setModifiedRoomName(event) { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + this.props.onChangeText(modifiedRoomName); + + // Prevent cursor jump behaviour: + // Check if newRoomNameWithHash is the same as modifiedRoomName + // If it is then the room name is valid (does not contain unallowed characters); no action required + // If not then the room name contains unvalid characters and we must adjust the cursor position manually + // Read more: https://github.com/Expensify/App/issues/12741 + const oldRoomNameWithHash = this.props.value || ''; + const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; + if (modifiedRoomName !== newRoomNameWithHash) { + const offset = modifiedRoomName.length - oldRoomNameWithHash.length; + const selection = { + start: this.state.selection.start + offset, + end: this.state.selection.end + offset, + }; + this.setSelection(selection); + } + } + + /** + * Set the selection + * @param {Object} selection + */ + setSelection(selection) { + this.setState({selection}); + } + + render() { + return ( + this.setSelection(event.nativeEvent.selection)} + errorText={this.props.errorText} + autoCapitalize="none" + /> + ); + } +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; + +export default withLocalize( + React.forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )), +); diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js new file mode 100644 index 000000000000..2eb22be46e18 --- /dev/null +++ b/src/components/RoomNameInput/index.native.js @@ -0,0 +1,50 @@ +import React, {Component} from 'react'; +import CONST from '../../CONST'; +import withLocalize from '../withLocalize'; +import TextInput from '../TextInput'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils'; + +class RoomNameInput extends Component { + constructor(props) { + super(props); + + this.setModifiedRoomName = this.setModifiedRoomName.bind(this); + } + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + setModifiedRoomName(event) { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + this.props.onChangeText(modifiedRoomName); + } + + render() { + return ( + + ); + } +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; + +export default withLocalize( + React.forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )), +); diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js new file mode 100644 index 000000000000..db5ce4b3c0c6 --- /dev/null +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import {withLocalizePropTypes} from '../withLocalize'; + +const propTypes = { + /** Callback to execute when the text input is modified correctly */ + onChangeText: PropTypes.func, + + /** Room name to show in input field. This should include the '#' already prefixed to the name */ + value: PropTypes.string, + + /** Whether we should show the input as disabled */ + disabled: PropTypes.bool, + + /** Error text to show */ + errorText: PropTypes.string, + + ...withLocalizePropTypes, + + /** A ref forwarded to the TextInput */ + forwardedRef: PropTypes.func, +}; + +const defaultProps = { + onChangeText: () => {}, + value: '', + disabled: false, + errorText: '', + forwardedRef: () => {}, +}; + +export {propTypes, defaultProps}; diff --git a/src/components/Section.js b/src/components/Section.js index a0fbf34f0833..83c8900a30a2 100644 --- a/src/components/Section.js +++ b/src/components/Section.js @@ -40,13 +40,13 @@ const Section = (props) => { const IconComponent = props.IconComponent; return ( <> - + {props.title} - {props.icon && } + {props.icon && } {IconComponent && } @@ -54,9 +54,12 @@ const Section = (props) => { {props.children} + + + {props.menuItems && } + - {props.menuItems && } ); }; diff --git a/src/components/TextLink.js b/src/components/TextLink.js index c7df1ce6cd59..17e73e02c819 100644 --- a/src/components/TextLink.js +++ b/src/components/TextLink.js @@ -22,20 +22,24 @@ const propTypes = { /** Overwrites the default link behavior with a custom callback */ onPress: PropTypes.func, + + /** Callback that is called when mousedown is triggered */ + onMouseDown: PropTypes.func, }; const defaultProps = { href: undefined, style: [], onPress: undefined, + onMouseDown: undefined, }; const TextLink = (props) => { const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; /** - * @param {Event} event - */ + * @param {Event} event + */ const openLink = (event) => { event.preventDefault(); if (props.onPress) { @@ -47,8 +51,8 @@ const TextLink = (props) => { }; /** - * @param {Event} event - */ + * @param {Event} event + */ const openLinkIfEnterKeyPressed = (event) => { if (event.key !== 'Enter') { return; @@ -62,6 +66,7 @@ const TextLink = (props) => { accessibilityRole="link" href={props.href} onPress={openLink} + onMouseDown={props.onMouseDown} onKeyDown={openLinkIfEnterKeyPressed} > {props.children} diff --git a/src/languages/en.js b/src/languages/en.js index d0524f20a9a7..f8f18a73e019 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -108,6 +108,8 @@ export default { maxParticipantsReached: ({count}) => `You've selected the maximum number (${count}) of participants.`, youAppearToBeOffline: 'You appear to be offline.', thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.', + areYouSure: 'Are you sure?', + zipCodeExample: 'e.g. 12345, 12345-1234, 12345 1234', }, attachmentPicker: { cameraPermissionRequired: 'Camera permission required', @@ -167,7 +169,6 @@ export default { sendAttachment: 'Send attachment', addAttachment: 'Add attachment', writeSomething: 'Write something...', - sayHello: 'Say hello!', conciergePlaceholderOptions: [ 'Ask for help!', 'Ask me anything!', @@ -362,6 +363,7 @@ export default { }, security: 'Security', signOut: 'Sign out', + signOutConfirmationText: 'You\'ll lose any offline changes if you sign-out.', versionLetter: 'v', readTheTermsAndPrivacyPolicy: { phrase1: 'Read the', diff --git a/src/languages/es.js b/src/languages/es.js index 7d60ec31a164..572859d59377 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -108,6 +108,8 @@ export default { maxParticipantsReached: ({count}) => `Has seleccionado el número máximo (${count}) de participantes.`, youAppearToBeOffline: 'Parece que estás desconectado.', thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa para ser utilizada.', + areYouSure: '¿Estás seguro?', + zipCodeExample: 'p. ej. 12345, 12345-1234, 12345 1234', }, attachmentPicker: { cameraPermissionRequired: 'Se necesita permiso para usar la cámara', @@ -167,7 +169,6 @@ export default { sendAttachment: 'Enviar adjunto', addAttachment: 'Agregar archivo adjunto', writeSomething: 'Escribe algo...', - sayHello: 'Di hola!', conciergePlaceholderOptions: [ '¡Pide ayuda!', '¡Pregúntame lo que sea!', @@ -362,6 +363,7 @@ export default { }, security: 'Seguridad', signOut: 'Desconectar', + signOutConfirmationText: 'Si cierras sesión perderás los cambios hechos mientras estabas desconectado', versionLetter: 'v', readTheTermsAndPrivacyPolicy: { phrase1: 'Leer los', diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js index b3fbbbd03d8a..56e406ba0ca5 100644 --- a/src/libs/DateUtils.js +++ b/src/libs/DateUtils.js @@ -163,10 +163,14 @@ function getMicroseconds() { /** * Returns the current time in milliseconds in the format expected by the database + * + * @param {String|Number} [timestamp] + * * @returns {String} */ -function currentDBTime() { - return new Date().toISOString() +function getDBTime(timestamp = '') { + const datetime = timestamp ? new Date(timestamp) : new Date(); + return datetime.toISOString() .replace('T', ' ') .replace('Z', ''); } @@ -183,7 +187,7 @@ const DateUtils = { canUpdateTimezone, setTimezoneUpdated, getMicroseconds, - currentDBTime, + getDBTime, }; export default DateUtils; diff --git a/src/libs/IOUUtils.js b/src/libs/IOUUtils.js index ada242b6c331..bfa603c9a6e3 100644 --- a/src/libs/IOUUtils.js +++ b/src/libs/IOUUtils.js @@ -1,14 +1,5 @@ -import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; -import ONYXKEYS from '../ONYXKEYS'; import CONST from '../CONST'; -let isNetworkOffline = false; -Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: val => isNetworkOffline = lodashGet(val, 'isOffline', false), -}); - /** * Calculates the amount per user given a list of participants * @param {Array} participants - List of logins for the participants in the chat. It should not include the current user's login. @@ -50,7 +41,7 @@ function calculateAmount(participants, total, isDefaultUser = false) { * @returns {Object} */ function updateIOUOwnerAndTotal(iouReport, actorEmail, amount, currency, type = CONST.IOU.REPORT_ACTION_TYPE.CREATE) { - if (currency !== iouReport.currency && isNetworkOffline) { + if (currency !== iouReport.currency) { return iouReport; } diff --git a/src/libs/LoginUtils.js b/src/libs/LoginUtils.js index 01cdb8b98922..8c0555af7b1a 100644 --- a/src/libs/LoginUtils.js +++ b/src/libs/LoginUtils.js @@ -31,27 +31,6 @@ function getPhoneNumberWithoutUSCountryCodeAndSpecialChars(phone) { return getPhoneNumberWithoutSpecialChars(phone.replace(/^\+1/, '')); } -/** - * Converts a login list to an object, where the login partnerUserID is the key - * - * TODO: remove this once the server is always sending back the correct format! - * https://github.com/Expensify/App/issues/10960 - * - * @param {Array|Object} loginList - * @return {Object} - */ -function convertLoginListToObject(loginList = {}) { - if (!_.isArray(loginList)) { - return loginList; - } - - return _.reduce(loginList, (allLogins, login) => { - // eslint-disable-next-line no-param-reassign - allLogins[login.partnerUserID] = login; - return allLogins; - }, {}); -} - /** * Filter out all non-Expensify partners from login list * @@ -63,21 +42,18 @@ function keepExpensifyPartners(loginList = {}) { } /** - * Cleans login list that came from the server by doing two steps: - * 1. Converts login list to an object (in case it came as an array) - * 2. Only keeps logins with Expensify partner name + * Cleans login list that came from the server by only keeping logins with Expensify partner name * - * @param {Array|Object} loginList + * @param {Object} loginList * @returns {Object} */ function cleanLoginListServerResponse(loginList = {}) { - return keepExpensifyPartners(convertLoginListToObject(loginList)); + return keepExpensifyPartners(loginList); } export { getEmailWithoutMergedAccountPrefix, getPhoneNumberWithoutSpecialChars, getPhoneNumberWithoutUSCountryCodeAndSpecialChars, - convertLoginListToObject, cleanLoginListServerResponse, }; diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js index cf16d687d555..433620deecb9 100644 --- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js +++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js @@ -28,15 +28,10 @@ export default ( cardStyle.transform = [{translateX}]; } - let containerStyle; - if (!isSmallScreenWidth) { - containerStyle = { - overflow: 'hidden', - }; - } - return ({ - containerStyle, + containerStyle: { + overflow: 'hidden', + }, cardStyle, overlayStyle: { opacity: progress.interpolate({ diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index faac86e5f6f2..96e70848cd3d 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -10,7 +10,6 @@ import * as Localize from './Localize'; import Permissions from './Permissions'; import * as CollectionUtils from './CollectionUtils'; import Navigation from './Navigation/Navigation'; -import * as LoginUtils from './LoginUtils'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -27,9 +26,7 @@ Onyx.connect({ let loginList; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (val) => { - loginList = LoginUtils.convertLoginListToObject(val); - }, + callback: val => loginList = _.isEmpty(val) ? {} : val, }); let countryCodeByIP; @@ -115,6 +112,11 @@ function getPersonalDetailsForLogins(logins, personalDetails) { avatar: ReportUtils.getDefaultAvatar(login), }; } + + if (login === CONST.EMAIL.CONCIERGE) { + personalDetail.avatar = CONST.CONCIERGE_ICON_URL; + } + personalDetailsForLogins[login] = personalDetail; }); return personalDetailsForLogins; @@ -467,10 +469,10 @@ function getOptions(reports, personalDetails, { // - All archived reports should remain at the bottom const orderedReports = _.sortBy(filteredReports, (report) => { if (ReportUtils.isArchivedRoom(report)) { - return -Infinity; + return CONST.DATE.UNIX_EPOCH; } - return report.lastMessageTimestamp; + return report.lastActionCreated; }); orderedReports.reverse(); diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 2876d36ebefb..9bcce5209ea2 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -22,6 +22,12 @@ Onyx.connect({ }, }); +let isNetworkOffline = false; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: val => isNetworkOffline = lodashGet(val, 'isOffline', false), +}); + /** * @param {Object} reportAction * @returns {Boolean} @@ -74,7 +80,9 @@ function getMostRecentIOUReportActionID(reportActions) { * @returns {Boolean} */ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) { - const previousAction = reportActions[actionIndex + 1]; + // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. + // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. + const previousAction = _.find(_.drop(reportActions, actionIndex + 1), action => isNetworkOffline || (action.action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)); const currentAction = reportActions[actionIndex]; // It's OK for there to be no previous action, and in that case, false will be returned diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index b0035909d1d4..989521490e54 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -75,6 +75,15 @@ function getReportParticipantsTitle(logins) { return _.map(logins, login => Str.removeSMSDomain(login)).join(', '); } +/** + * Attempts to find a report in onyx with the provided list of participants + * @param {Object} report + * @returns {Boolean} + */ +function isIOUReport(report) { + return report && _.has(report, 'total'); +} + /** * Check whether a report action is Attachment or not. * Ignore messages containing [Attachment] as the main content. Attachments are actions with only text as [Attachment]. @@ -95,7 +104,7 @@ function isReportMessageAttachment({text, html}) { function sortReportsByLastVisited(reports) { return _.chain(reports) .toArray() - .filter(report => report && report.reportID) + .filter(report => report && report.reportID && !isIOUReport(report)) .sortBy('lastVisitedTimestamp') .value(); } @@ -274,7 +283,7 @@ function getPolicyName(report, policies) { const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; if (!policy) { - return Localize.translateLocal('workspace.common.unavailable'); + return report.oldPolicyName || Localize.translateLocal('workspace.common.unavailable'); } return policy.name @@ -643,7 +652,7 @@ function buildOptimisticReportAction(sequenceNumber, text, file) { sequenceNumber, clientID: NumberUtils.generateReportActionClientID(), avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)), - created: DateUtils.currentDBTime(), + created: DateUtils.getDBTime(), message: [ { type: CONST.REPORT.MESSAGE.TYPE.COMMENT, @@ -795,7 +804,7 @@ function buildOptimisticIOUReportAction(sequenceNumber, type, amount, currency, reportActionID: NumberUtils.rand64(), sequenceNumber, shouldShow: true, - created: DateUtils.currentDBTime(), + created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; } @@ -834,7 +843,7 @@ function buildOptimisticChatReport( lastMessageHtml: '', lastMessageText: null, lastReadSequenceNumber: 0, - lastMessageTimestamp: 0, + lastActionCreated: '', lastVisitedTimestamp: 0, maxSequenceNumber: 0, notificationPreference, @@ -883,7 +892,7 @@ function buildOptimisticCreatedReportAction(ownerEmail) { automatic: false, sequenceNumber: 0, avatar: lodashGet(allPersonalDetails, [currentUserEmail, 'avatar'], getDefaultAvatar(currentUserEmail)), - created: DateUtils.currentDBTime(), + created: DateUtils.getDBTime(), shouldShow: true, }, }; diff --git a/src/libs/RoomNameInputUtils.js b/src/libs/RoomNameInputUtils.js new file mode 100644 index 000000000000..5522096973de --- /dev/null +++ b/src/libs/RoomNameInputUtils.js @@ -0,0 +1,24 @@ +import CONST from '../CONST'; + +/** + * Modifies the room name to follow our conventions: + * - Max length 80 characters + * - Cannot not include space or special characters, and we automatically apply an underscore for spaces + * - Must be lowercase + * @param {String} roomName + * @returns {String} + */ +function modifyRoomName(roomName) { + const modifiedRoomNameWithoutHash = roomName + .replace(/ /g, '_') + .replace(/[^a-zA-Z\d_]/g, '') + .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) + .toLowerCase(); + + return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; +} + +export { + // eslint-disable-next-line import/prefer-default-export + modifyRoomName, +}; diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 0a0f35531902..b0932f85b1e7 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -114,10 +114,10 @@ function getOrderedReportIDs(reportIDFromRoute) { // 2. Outstanding IOUs - Always sorted by iouReportAmount with the largest amounts at the top of the group // 3. Drafts - Always sorted by reportDisplayName // 4. Non-archived reports - // - Sorted by lastMessageTimestamp in default (most recent) view mode + // - Sorted by lastActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode // 5. Archived reports - // - Sorted by lastMessageTimestamp in default (most recent) view mode + // - Sorted by lastActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode let pinnedReports = []; let outstandingIOUReports = []; @@ -153,8 +153,8 @@ function getOrderedReportIDs(reportIDFromRoute) { pinnedReports = _.sortBy(pinnedReports, report => report.displayName.toLowerCase()); outstandingIOUReports = _.sortBy(outstandingIOUReports, 'iouReportAmount').reverse(); draftReports = _.sortBy(draftReports, report => report.displayName.toLowerCase()); - nonArchivedReports = _.sortBy(nonArchivedReports, report => (isInDefaultMode ? report.lastMessageTimestamp : report.displayName.toLowerCase())); - archivedReports = _.sortBy(archivedReports, report => (isInDefaultMode ? report.lastMessageTimestamp : report.displayName.toLowerCase())); + nonArchivedReports = _.sortBy(nonArchivedReports, report => (isInDefaultMode ? report.lastActionCreated : report.displayName.toLowerCase())); + archivedReports = _.sortBy(archivedReports, report => (isInDefaultMode ? report.lastActionCreated : report.displayName.toLowerCase())); // For archived and non-archived reports, ensure that most recent reports are at the top by reversing the order of the arrays because underscore will only sort them in ascending order if (isInDefaultMode) { diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 438236afdefa..9fb462f64b50 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -113,7 +113,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com }, }, { - onyxMethod: CONST.ONYX.METHOD.MERGE, + onyxMethod: originalIOUStatus ? CONST.ONYX.METHOD.MERGE : CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_IOUS}${iouReport.reportID}`, value: iouReport, }, diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 0acaa30be438..9f3edbde34a8 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -752,7 +752,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, - value: announceChatData, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...announceChatData, + }, }, { onyxMethod: CONST.ONYX.METHOD.SET, @@ -762,7 +767,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, - value: adminsChatData, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...adminsChatData, + }, }, { onyxMethod: CONST.ONYX.METHOD.SET, @@ -772,7 +782,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: expenseChatData, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...expenseChatData, + }, }, { onyxMethod: CONST.ONYX.METHOD.SET, @@ -787,7 +802,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceChatReportID}`, - value: {pendingAction: null}, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, @@ -801,7 +821,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, - value: {pendingAction: null}, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, @@ -815,7 +840,12 @@ function createWorkspace(ownerEmail = '', makeMeAdmin = false, policyName = '', { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: {pendingAction: null}, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, }, { onyxMethod: CONST.ONYX.METHOD.MERGE, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 164d0e60bbbb..d6c5220d2785 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -90,8 +90,7 @@ function getParticipantEmailsFromReport({sharedReportList, reportNameValuePairs, * @returns {Object} */ function getSimplifiedReportObject(report) { - const createTimestamp = lodashGet(report, 'lastActionCreated', 0); - const lastMessageTimestamp = moment.utc(createTimestamp).unix(); + const lastActionCreated = lodashGet(report, 'lastActionCreated', 0); const lastActionMessage = lodashGet(report, ['lastActionMessage', 'html'], ''); const isLastMessageAttachment = new RegExp(`]*${CONST.ATTACHMENT_SOURCE_ATTRIBUTE}\\s*=\\s*"[^"]*"[^>]*>`, 'gi').test(lastActionMessage); const chatType = lodashGet(report, ['reportNameValuePairs', 'chatType'], ''); @@ -135,7 +134,7 @@ function getSimplifiedReportObject(report) { 'timestamp', ], 0), lastReadSequenceNumber, - lastMessageTimestamp, + lastActionCreated, lastMessageText: isLastMessageAttachment ? '[Attachment]' : lastMessageText, lastActorEmail, notificationPreference, @@ -414,7 +413,7 @@ function addActions(reportID, text = '', file) { // Update the report in Onyx to have the new sequence number const optimisticReport = { maxSequenceNumber: newSequenceNumber, - lastMessageTimestamp: Date.now(), + lastActionCreated: DateUtils.getDBTime(), lastMessageText: ReportUtils.formatReportLastMessageText(lastAction.message[0].text), lastActorEmail: currentUserEmail, lastReadSequenceNumber: newSequenceNumber, diff --git a/src/libs/getEmailKeyboardType/index.js b/src/libs/getEmailKeyboardType/index.js deleted file mode 100644 index 9d77eb437853..000000000000 --- a/src/libs/getEmailKeyboardType/index.js +++ /dev/null @@ -1,6 +0,0 @@ - -/** - * Return the default keyboard type when running on Web/Desktop - * @return {String} - */ -export default () => 'default'; diff --git a/src/libs/getEmailKeyboardType/index.native.js b/src/libs/getEmailKeyboardType/index.native.js deleted file mode 100644 index 5f599436c8ae..000000000000 --- a/src/libs/getEmailKeyboardType/index.native.js +++ /dev/null @@ -1,6 +0,0 @@ - -/** - * Return the email-address keyboard type when running on native platforms. - * @return {String} - */ -export default () => 'email-address'; diff --git a/src/libs/migrations/AddLastActionCreated.js b/src/libs/migrations/AddLastActionCreated.js new file mode 100644 index 000000000000..38c6a715614b --- /dev/null +++ b/src/libs/migrations/AddLastActionCreated.js @@ -0,0 +1,46 @@ +import _ from 'underscore'; +import Onyx from 'react-native-onyx'; +import Log from '../Log'; +import ONYXKEYS from '../../ONYXKEYS'; + +/** + * This migration adds lastActionCreated to all reports in Onyx, using the value of lastMessageTimestamp + * + * @returns {Promise} + */ +export default function () { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallbacks: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + const reportsToUpdate = {}; + _.each(allReports, (report, key) => { + if (_.has(report, 'lastActionCreated')) { + return; + } + + if (!_.has(report, 'lastMessageTimestamp')) { + return; + } + + reportsToUpdate[key] = report; + reportsToUpdate[key].lastActionCreated = new Date(report.lastMessageTimestamp) + .toISOString() + .replace('T', ' ') + .replace('Z', ''); + }); + + if (_.isEmpty(reportsToUpdate)) { + Log.info('[Migrate Onyx] Skipped migration AddLastActionCreated'); + } else { + Log.info(`[Migrate Onyx] Adding lastActionCreated field to ${_.keys(reportsToUpdate).length} reports`); + // eslint-disable-next-line rulesdir/prefer-actions-set-data + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, reportsToUpdate); + } + }, + }); + return resolve(); + }); +} diff --git a/src/pages/GetAssistancePage.js b/src/pages/GetAssistancePage.js index 577a7dc5f6a1..fbf7ab90bd3a 100644 --- a/src/pages/GetAssistancePage.js +++ b/src/pages/GetAssistancePage.js @@ -7,6 +7,7 @@ import HeaderWithCloseButton from '../components/HeaderWithCloseButton'; import Section from '../components/Section'; import Navigation from '../libs/Navigation/Navigation'; import styles from '../styles/styles'; +import themeColors from '../styles/themes/default'; import Text from '../components/Text'; import * as Expensicons from '../components/Icon/Expensicons'; import * as Illustrations from '../components/Icon/Illustrations'; @@ -36,23 +37,27 @@ const GetAssistancePage = props => (
Report.navigateToConciergeChat(), icon: Expensicons.ChatBubble, shouldShowRightIcon: true, + iconFill: themeColors.success, + wrapperStyle: [styles.cardMenuItem], }, { title: props.translate('getAssistancePage.requestSetupCall'), onPress: () => Navigation.navigate(ROUTES.getRequestCallRoute(props.route.params.taskID)), icon: Expensicons.Phone, shouldShowRightIcon: true, + iconFill: themeColors.success, + wrapperStyle: [styles.cardMenuItem], }, ]} > - + {props.translate('getAssistancePage.description')}
diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 963ac5a9f524..5f79c634d930 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -84,13 +84,16 @@ class NewChatPage extends Component { */ getSections(maxParticipantsReached) { const sections = []; + let indexOffset = 0; + if (this.props.isGroupChat) { sections.push({ title: undefined, data: this.state.selectedOptions, shouldShow: !_.isEmpty(this.state.selectedOptions), - indexOffset: 0, + indexOffset, }); + indexOffset += this.state.selectedOptions.length; if (maxParticipantsReached) { return sections; @@ -107,22 +110,24 @@ class NewChatPage extends Component { title: this.props.translate('common.recents'), data: recentReportsWithoutSelected, shouldShow: !_.isEmpty(recentReportsWithoutSelected), - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += recentReportsWithoutSelected.length; sections.push({ title: this.props.translate('common.contacts'), data: personalDetailsWithoutSelected, shouldShow: !_.isEmpty(personalDetailsWithoutSelected), - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += personalDetailsWithoutSelected.length; if (hasUnselectedUserToInvite) { sections.push(({ title: undefined, data: [this.state.userToInvite], shouldShow: true, - indexOffset: 0, + indexOffset, })); } diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 9e734a70d403..bccf706a16d9 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -15,6 +15,21 @@ const propTypes = { /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ onFieldChange: PropTypes.func, + /** Default values */ + defaultValues: PropTypes.shape({ + /** Address street field */ + street: PropTypes.string, + + /** Address city field */ + city: PropTypes.string, + + /** Address state field */ + state: PropTypes.string, + + /** Address zip code field */ + zipCode: PropTypes.string, + }), + /** Form values */ values: PropTypes.shape({ /** Address street field */ @@ -55,6 +70,12 @@ const defaultProps = { state: undefined, zipCode: undefined, }, + defaultValues: { + street: undefined, + city: undefined, + state: undefined, + zipCode: undefined, + }, errors: {}, inputKeys: { street: '', @@ -75,6 +96,7 @@ const AddressForm = props => ( label={props.translate(props.streetTranslationKey)} containerStyles={[styles.mt4]} value={props.values.street} + defaultValue={props.defaultValues.street} onInputChange={props.onFieldChange} errorText={props.errors.street ? props.translate('bankAccount.error.addressStreet') : ''} hint={props.translate('common.noPO')} @@ -88,6 +110,7 @@ const AddressForm = props => ( shouldSaveDraft={props.shouldSaveDraft} label={props.translate('common.city')} value={props.values.city} + defaultValue={props.defaultValues.city} onChangeText={value => props.onFieldChange({city: value})} errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} /> @@ -97,6 +120,7 @@ const AddressForm = props => ( inputID={props.inputKeys.state} shouldSaveDraft={props.shouldSaveDraft} value={props.values.state} + defaultValue={props.defaultValues.state} onInputChange={value => props.onFieldChange({state: value})} errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} /> @@ -109,9 +133,11 @@ const AddressForm = props => ( containerStyles={[styles.mt4]} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} value={props.values.zipCode} + defaultValue={props.defaultValues.zipCode} onChangeText={value => props.onFieldChange({zipCode: value})} errorText={props.errors.zipCode ? props.translate('bankAccount.error.zipCode') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + hint={props.translate('common.zipCodeExample')} /> ); diff --git a/src/pages/ReimbursementAccount/BankAccountManualStep.js b/src/pages/ReimbursementAccount/BankAccountManualStep.js index c42b0f7186f4..45c73b109b33 100644 --- a/src/pages/ReimbursementAccount/BankAccountManualStep.js +++ b/src/pages/ReimbursementAccount/BankAccountManualStep.js @@ -119,7 +119,12 @@ class BankAccountManualStep extends React.Component { {this.props.translate('common.iAcceptThe')} - + e.preventDefault()} + > {`Expensify ${this.props.translate('common.termsOfService')}`}
diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 3a71ac53b9d7..d2e5fdf9ecfa 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -9,6 +9,7 @@ import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import MenuItem from '../../components/MenuItem'; import * as Expensicons from '../../components/Icon/Expensicons'; import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; import TextLink from '../../components/TextLink'; import Icon from '../../components/Icon'; import colors from '../../styles/colors'; @@ -98,45 +99,50 @@ const BankAccountStep = (props) => { />
- - {props.translate('bankAccount.toGetStarted')} - - {plaidDesktopMessage && ( - - - {props.translate(plaidDesktopMessage)} - + > + + {props.translate('bankAccount.toGetStarted')} - )} -
); } diff --git a/src/pages/ReimbursementAccount/IdentityForm.js b/src/pages/ReimbursementAccount/IdentityForm.js index 1c06061a44cb..95994b73d7f8 100644 --- a/src/pages/ReimbursementAccount/IdentityForm.js +++ b/src/pages/ReimbursementAccount/IdentityForm.js @@ -42,6 +42,33 @@ const propTypes = { ssnLast4: PropTypes.string, }), + /** Default values */ + defaultValues: PropTypes.shape({ + /** First name field */ + firstName: PropTypes.string, + + /** Last name field */ + lastName: PropTypes.string, + + /** Address street field */ + street: PropTypes.string, + + /** Address city field */ + city: PropTypes.string, + + /** Address state field */ + state: PropTypes.string, + + /** Address zip code field */ + zipCode: PropTypes.string, + + /** Date of birth field */ + dob: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + + /** Last 4 digits of SSN */ + ssnLast4: PropTypes.string, + }), + /** Any errors that can arise from form validation */ errors: PropTypes.objectOf(PropTypes.bool), @@ -76,6 +103,16 @@ const defaultProps = { dob: undefined, ssnLast4: undefined, }, + defaultValues: { + firstName: undefined, + lastName: undefined, + street: undefined, + city: undefined, + state: undefined, + zipCode: undefined, + dob: undefined, + ssnLast4: undefined, + }, errors: {}, inputKeys: { firstName: '', @@ -95,6 +132,7 @@ const IdentityForm = (props) => { // dob field has multiple validations/errors, we are handling it temporarily like this. const dobErrorText = (props.errors.dob ? props.translate('bankAccount.error.dob') : '') || (props.errors.dobAge ? props.translate('bankAccount.error.age') : ''); + const identityFormInputKeys = ['firstName', 'lastName', 'dob', 'ssnLast4']; return ( @@ -105,6 +143,7 @@ const IdentityForm = (props) => { shouldSaveDraft={props.shouldSaveDraft} label={`${props.translate('common.firstName')}`} value={props.values.firstName} + defaultValue={props.defaultValues.firstName} onChangeText={value => props.onFieldChange({firstName: value})} errorText={props.errors.firstName ? props.translate('bankAccount.error.firstName') : ''} /> @@ -115,6 +154,7 @@ const IdentityForm = (props) => { shouldSaveDraft={props.shouldSaveDraft} label={`${props.translate('common.lastName')}`} value={props.values.lastName} + defaultValue={props.defaultValues.lastName} onChangeText={value => props.onFieldChange({lastName: value})} errorText={props.errors.lastName ? props.translate('bankAccount.error.lastName') : ''} /> @@ -126,7 +166,7 @@ const IdentityForm = (props) => { label={`${props.translate('common.dob')}`} containerStyles={[styles.mt4]} placeholder={props.translate('common.dateFormat')} - defaultValue={props.values.dob} + defaultValue={props.values.dob || props.defaultValues.dob} onInputChange={value => props.onFieldChange({dob: value})} errorText={dobErrorText} maximumDate={new Date()} @@ -137,17 +177,18 @@ const IdentityForm = (props) => { label={`${props.translate('common.ssnLast4')}`} containerStyles={[styles.mt4]} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - value={props.values.ssnLast4} + defaultValue={props.defaultValues.ssnLast4} onChangeText={value => props.onFieldChange({ssnLast4: value})} errorText={props.errors.ssnLast4 ? props.translate('bankAccount.error.ssnLast4') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} /> diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js index 05fc624d61be..474ed78b4281 100644 --- a/src/pages/ReimbursementAccount/RequestorStep.js +++ b/src/pages/ReimbursementAccount/RequestorStep.js @@ -2,7 +2,6 @@ import React from 'react'; import lodashGet from 'lodash/get'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import moment from 'moment'; import PropTypes from 'prop-types'; import styles from '../../styles/styles'; @@ -18,21 +17,15 @@ import IdentityForm from './IdentityForm'; import * as ValidationUtils from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; -import * as ReimbursementAccountUtils from '../../libs/ReimbursementAccountUtils'; import reimbursementAccountPropTypes from './reimbursementAccountPropTypes'; -import reimbursementAccountDraftPropTypes from './ReimbursementAccountDraftPropTypes'; -import ReimbursementAccountForm from './ReimbursementAccountForm'; import * as Link from '../../libs/actions/Link'; import RequestorOnfidoStep from './RequestorOnfidoStep'; +import Form from '../../components/Form'; const propTypes = { /** The bank account currently in setup */ reimbursementAccount: reimbursementAccountPropTypes.isRequired, - /** The draft values of the bank account being setup */ - /* eslint-disable-next-line react/no-unused-prop-types */ - reimbursementAccountDraft: reimbursementAccountDraftPropTypes.isRequired, - /** The token required to initialize the Onfido SDK */ onfidoToken: PropTypes.string, @@ -47,38 +40,14 @@ class RequestorStep extends React.Component { constructor(props) { super(props); + this.getDefaultStateForField = this.getDefaultStateForField.bind(this); + this.validate = this.validate.bind(this); this.submit = this.submit.bind(this); - this.clearErrorsAndSetValues = this.clearErrorsAndSetValues.bind(this); this.setOnfidoAsComplete = this.setOnfidoAsComplete.bind(this); this.state = { - firstName: ReimbursementAccountUtils.getDefaultStateForField(props, 'firstName'), - lastName: ReimbursementAccountUtils.getDefaultStateForField(props, 'lastName'), - requestorAddressStreet: ReimbursementAccountUtils.getDefaultStateForField(props, 'requestorAddressStreet'), - requestorAddressCity: ReimbursementAccountUtils.getDefaultStateForField(props, 'requestorAddressCity'), - requestorAddressState: ReimbursementAccountUtils.getDefaultStateForField(props, 'requestorAddressState'), - requestorAddressZipCode: ReimbursementAccountUtils.getDefaultStateForField(props, 'requestorAddressZipCode'), - dob: ReimbursementAccountUtils.getDefaultStateForField(props, 'dob'), - ssnLast4: ReimbursementAccountUtils.getDefaultStateForField(props, 'ssnLast4'), - isControllingOfficer: ReimbursementAccountUtils.getDefaultStateForField(props, 'isControllingOfficer', false), isOnfidoSetupComplete: lodashGet(props, ['achData', 'isOnfidoSetupComplete'], false), }; - - // Required fields not validated by `validateIdentity` - this.requiredFields = [ - 'isControllingOfficer', - ]; - - // Map a field to the key of the error's translation - this.errorTranslationKeys = { - firstName: 'bankAccount.error.firstName', - lastName: 'bankAccount.error.lastName', - isControllingOfficer: 'requestorStep.isControllingOfficerError', - }; - - this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); - this.clearErrors = inputKeys => ReimbursementAccountUtils.clearErrors(this.props, inputKeys); - this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); } /** @@ -89,73 +58,74 @@ class RequestorStep extends React.Component { } /** - * Clear the errors associated to keys in values if found and store the new values in the state. - * + * Get default value from reimbursementAccount or achData + * @param {String} fieldName + * @param {*} defaultValue + * @returns {String} + */ + getDefaultStateForField(fieldName, defaultValue) { + return lodashGet(this.props, ['reimbursementAccount', 'achData', fieldName], defaultValue); + } + + /** * @param {Object} values + * @returns {Object} */ - clearErrorsAndSetValues(values) { - const renamedFields = { - street: 'requestorAddressStreet', - city: 'requestorAddressCity', - state: 'requestorAddressState', - zipCode: 'requestorAddressZipCode', - }; - const newState = {}; - _.each(values, (value, inputKey) => { - const renamedInputKey = lodashGet(renamedFields, inputKey, inputKey); - newState[renamedInputKey] = value; - }); - this.setState(newState); - BankAccounts.updateReimbursementAccountDraft(newState); + validate(values) { + const errors = {}; - // Prepare inputKeys for clearing errors - const inputKeys = _.keys(values); + if (!ValidationUtils.isRequiredFulfilled(values.firstName)) { + errors.firstName = this.props.translate('bankAccount.error.firstName'); + } - // dob field has multiple validations/errors, we are handling it temporarily like this. - if (_.contains(inputKeys, 'dob')) { - inputKeys.push('dobAge'); + if (!ValidationUtils.isRequiredFulfilled(values.lastName)) { + errors.lastName = this.props.translate('bankAccount.error.lastName'); } - this.clearErrors(inputKeys); - } - /** - * @returns {Boolean} - */ - validate() { - const errors = ValidationUtils.validateIdentity({ - firstName: this.state.firstName, - lastName: this.state.lastName, - street: this.state.requestorAddressStreet, - state: this.state.requestorAddressState, - city: this.state.requestorAddressCity, - zipCode: this.state.requestorAddressZipCode, - dob: this.state.dob, - ssnLast4: this.state.ssnLast4, - }); + if (!ValidationUtils.isRequiredFulfilled(values.dob)) { + errors.dob = this.props.translate('bankAccount.error.dob'); + } - _.each(this.requiredFields, (inputKey) => { - if (ValidationUtils.isRequiredFulfilled(this.state[inputKey])) { - return; - } + if (values.dob && !ValidationUtils.meetsAgeRequirements(values.dob)) { + errors.dob = this.props.translate('bankAccount.error.age'); + } - errors[inputKey] = true; - }); - if (_.size(errors)) { - BankAccounts.setBankAccountFormValidationErrors(errors); - return false; + if (!ValidationUtils.isRequiredFulfilled(values.ssnLast4) || !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { + errors.ssnLast4 = this.props.translate('bankAccount.error.ssnLast4'); } - return true; - } - submit() { - if (!this.validate()) { - return; + if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressStreet)) { + errors.requestorAddressStreet = this.props.translate('bankAccount.error.address'); } + if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { + errors.requestorAddressStreet = this.props.translate('bankAccount.error.addressStreet'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressCity)) { + errors.requestorAddressCity = this.props.translate('bankAccount.error.addressCity'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressState)) { + errors.requestorAddressState = this.props.translate('bankAccount.error.addressState'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.requestorAddressZipCode) || !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { + errors.requestorAddressZipCode = this.props.translate('bankAccount.error.zipCode'); + } + + if (!ValidationUtils.isRequiredFulfilled(values.isControllingOfficer)) { + errors.isControllingOfficer = this.props.translate('requestorStep.isControllingOfficerError'); + } + + return errors; + } + + submit(values) { const payload = { - bankAccountID: ReimbursementAccountUtils.getDefaultStateForField(this.props, 'bankAccountID', 0), - ...this.state, - dob: moment(this.state.dob).format(CONST.DATE.MOMENT_FORMAT_STRING), + bankAccountID: this.getDefaultStateForField('bankAccountID', 0), + ...values, + dob: moment(values.dob).format(CONST.DATE.MOMENT_FORMAT_STRING), }; BankAccounts.updatePersonalInformationForBankAccount(payload); @@ -187,8 +157,12 @@ class RequestorStep extends React.Component { onComplete={this.setOnfidoAsComplete} /> ) : ( - {this.props.translate('requestorStep.subtitle')} @@ -209,30 +183,32 @@ class RequestorStep extends React.Component { { - this.setState((prevState) => { - const newState = {isControllingOfficer: !prevState.isControllingOfficer}; - BankAccounts.updateReimbursementAccountDraft(newState); - return newState; - }); - this.clearError('isControllingOfficer'); - }} + inputID="isControllingOfficer" + defaultValue={this.getDefaultStateForField('isControllingOfficer', false)} LabelComponent={() => ( @@ -241,7 +217,7 @@ class RequestorStep extends React.Component { )} style={[styles.mt4]} - errorText={this.getErrors().isControllingOfficer ? this.props.translate('requestorStep.isControllingOfficerError') : ''} + shouldSaveDraft /> {this.props.translate('requestorStep.onFidoConditions')} @@ -269,7 +245,7 @@ class RequestorStep extends React.Component { {`${this.props.translate('common.termsOfService')}`} - + )} ); @@ -288,8 +264,5 @@ export default compose( onfidoToken: { key: ONYXKEYS.ONFIDO_TOKEN, }, - reimbursementAccountDraft: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, - }, }), )(RequestorStep); diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js index 131b3add37d1..97c3d62d54b0 100644 --- a/src/pages/ReimbursementAccount/ValidationStep.js +++ b/src/pages/ReimbursementAccount/ValidationStep.js @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import _ from 'underscore'; import styles from '../../styles/styles'; +import themeColors from '../../styles/themes/default'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import * as Report from '../../libs/actions/Report'; @@ -188,28 +189,30 @@ class ValidationStep extends React.Component {
{this.props.translate('validationStep.letsChatText')} + +
- -
)}
diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js index c59511daff67..c2c35e4c7c7b 100644 --- a/src/pages/ReportSettingsPage.js +++ b/src/pages/ReportSettingsPage.js @@ -102,6 +102,11 @@ class ReportSettingsPage extends Component { return false; } + // Show error if the room name already exists + if (ValidationUtils.isExistingRoomName(this.state.newRoomName, this.props.reports, this.props.report.policyID)) { + errors.newRoomName = this.props.translate('newRoomPage.roomAlreadyExistsError'); + } + // We error if the user doesn't enter a room name or left blank if (!this.state.newRoomName || this.state.newRoomName === CONST.POLICY.ROOM_PREFIX) { errors.newRoomName = this.props.translate('newRoomPage.pleaseEnterRoomName'); @@ -253,5 +258,8 @@ export default compose( policies: { key: ONYXKEYS.COLLECTION.POLICY, }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, }), )(ReportSettingsPage); diff --git a/src/pages/RequestCallPage.js b/src/pages/RequestCallPage.js index 6cb145f30aad..b46a4ecfc5b0 100644 --- a/src/pages/RequestCallPage.js +++ b/src/pages/RequestCallPage.js @@ -31,23 +31,15 @@ import networkPropTypes from '../components/networkPropTypes'; import RequestCallConfirmationScreen from './RequestCallConfirmationScreen'; import Form from '../components/Form'; -const loginPropTypes = PropTypes.shape({ - /** Phone/Emails associated with user */ - partnerUserID: PropTypes.string, -}); - const propTypes = { ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, /** Login list for the user that is signed in */ - loginList: PropTypes.oneOfType([ - PropTypes.objectOf(loginPropTypes), - - // TODO: remove this once this closes: - // https://github.com/Expensify/App/issues/10960 - PropTypes.arrayOf(loginPropTypes), - ]), + loginList: PropTypes.shape({ + /** Phone/Emails associated with user */ + partnerUserID: PropTypes.string, + }), /** The policies which the user has access to */ policies: PropTypes.shape({ @@ -162,14 +154,13 @@ class RequestCallPage extends Component { } /** - * Gets the user's phone number from their secondary login. - * Returns null if it doesn't exist. + * Gets the user's phone number from their secondary logins. + * Returns empty string if it doesn't exist. * - * @param {Array|Object} loginList - * @returns {String|null} + * @returns {String} */ - getPhoneNumber(loginList) { - const secondaryLogin = _.find(_.values(LoginUtils.convertLoginListToObject(loginList)), login => Str.isSMSLogin(login.partnerUserID)); + getPhoneNumber() { + const secondaryLogin = _.find(_.values(this.props.loginList), login => Str.isSMSLogin(login.partnerUserID)); return secondaryLogin ? Str.removeSMSDomain(secondaryLogin.partnerUserID) : ''; } @@ -274,10 +265,10 @@ class RequestCallPage extends Component { >
- + {this.props.translate('requestCallPage.description')}
@@ -299,7 +290,7 @@ class RequestCallPage extends Component { /> 0) { sections.push(({ data: this.state.recentReports, shouldShow: true, - indexOffset: 0, + indexOffset, })); + indexOffset += this.state.recentReports.length; } if (this.state.personalDetails.length > 0) { sections.push(({ data: this.state.personalDetails, shouldShow: true, - indexOffset: this.state.recentReports.length, + indexOffset, })); + indexOffset += this.state.recentReports.length; } if (this.state.userToInvite) { @@ -108,7 +112,7 @@ class SearchPage extends Component { undefined, data: [this.state.userToInvite], shouldShow: true, - indexOffset: 0, + indexOffset, })); } @@ -180,7 +184,6 @@ class SearchPage extends Component { hideAdditionalOptionStates showTitleTooltip shouldShowOptions={didScreenTransitionEnd} - shouldDelayFocus /> diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 972dc0210f2b..fd2260a898b1 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -216,15 +216,23 @@ class ReportScreen extends React.Component { // There are no reportActions at all to display and we are still in the process of loading the next set of actions. const isLoadingInitialReportActions = _.isEmpty(this.props.reportActions) && this.props.report.isLoadingReportActions; + + // When the ReportScreen is not open/in the viewport, we want to "freeze" it for performance reasons + const freeze = this.props.isSmallScreenWidth && this.props.isDrawerOpen; + + // the moment the ReportScreen becomes unfrozen we want to start the animation of the placeholder skeleton content + // (which is shown, until all the actual views of the ReportScreen have been rendered) + const animatePlaceholder = !freeze; + return ( - - + + )} > diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index 97287442ce29..2d0067217c1b 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -79,7 +79,7 @@ class FloatingMessageCounter extends PureComponent { onPress={this.props.onClick} ContentComponent={() => ( - + ; - } - if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { - return ; - } - + /** + * Get the content of ReportActionItem + * @param {Boolean} hovered whether the ReportActionItem is hovered + * @returns {Object} child component(s) + */ + renderItemContent(hovered = false) { let children; if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { children = ( @@ -140,6 +139,7 @@ class ReportActionItem extends Component { chatReportID={this.props.report.reportID} action={this.props.action} isMostRecentIOUReportAction={this.props.isMostRecentIOUReportAction} + isHovered={hovered} /> ); } else { @@ -161,6 +161,16 @@ class ReportActionItem extends Component { /> ); } + return children; + } + + render() { + if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + return ; + } + if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return ; + } return ( this.popoverAnchor = el} @@ -198,12 +208,12 @@ class ReportActionItem extends Component { {!this.props.displayAsGroup ? ( - {children} + {this.renderItemContent(hovered || this.state.isContextMenuActive)} ) : ( - {children} + {this.renderItemContent(hovered || this.state.isContextMenuActive)} )} diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index f2a8ed496220..3bdb07d86615 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -42,7 +42,7 @@ const propTypes = { /** All report actions for all reports */ // eslint-disable-next-line react/no-unused-prop-types - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + reportActions: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes))), /** List of users' personal details */ personalDetails: PropTypes.objectOf(participantPropTypes), @@ -213,7 +213,7 @@ const reportSelector = report => report && ({ maxSequenceNumber: report.maxSequenceNumber, lastReadSequenceNumber: report.lastReadSequenceNumber, lastMessageText: report.lastMessageText, - lastMessageTimestamp: report.lastMessageTimestamp, + lastActionCreated: report.lastActionCreated, iouReportID: report.iouReportID, hasOutstandingIOU: report.hasOutstandingIOU, statusNum: report.statusNum, diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index d5b555f40338..2621b19ce81f 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -126,7 +126,6 @@ class IOUCurrencySelection extends Component { onChangeText={this.changeSearchValue} placeholderText={this.props.translate('common.search')} headerMessage={headerMessage} - shouldDelayFocus /> ); diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js index 5968242bcbac..fb68f8571fa1 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsRequest.js @@ -64,27 +64,30 @@ class IOUParticipantsRequest extends Component { */ getSections() { const sections = []; + let indexOffset = 0; sections.push({ title: this.props.translate('common.recents'), data: this.state.recentReports, shouldShow: !_.isEmpty(this.state.recentReports), - indexOffset: 0, + indexOffset, }); + indexOffset += this.state.recentReports.length; sections.push({ title: this.props.translate('common.contacts'), data: this.state.personalDetails, shouldShow: !_.isEmpty(this.state.personalDetails), - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += this.state.personalDetails.length; if (this.state.userToInvite && !OptionsListUtils.isCurrentUser(this.state.userToInvite)) { sections.push({ undefined, data: [this.state.userToInvite], shouldShow: true, - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); } @@ -136,7 +139,6 @@ class IOUParticipantsRequest extends Component { headerMessage={headerMessage} hideAdditionalOptionStates forceTextUnreadStyle - shouldDelayFocus /> ); } diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js index eacefbeedceb..76a694b664ae 100755 --- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js +++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js @@ -87,12 +87,15 @@ class IOUParticipantsSplit extends Component { */ getSections(maxParticipantsReached) { const sections = []; + let indexOffset = 0; + sections.push({ title: undefined, data: this.props.participants, shouldShow: true, - indexOffset: 0, + indexOffset, }); + indexOffset += this.props.participants.length; if (maxParticipantsReached) { return sections; @@ -102,28 +105,24 @@ class IOUParticipantsSplit extends Component { title: this.props.translate('common.recents'), data: this.state.recentReports, shouldShow: !_.isEmpty(this.state.recentReports), - - // takes the sum off the length of all data - // (this.state.selectedOptions) in previous sections - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += this.state.recentReports.length; sections.push({ title: this.props.translate('common.contacts'), data: this.state.personalDetails, shouldShow: !_.isEmpty(this.state.personalDetails), - - // takes the sum off the length of all data - // (this.state.selectedOptions, this.state.recentReports) in previous sections - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += this.state.personalDetails.length; if (this.state.userToInvite && !OptionsListUtils.isCurrentUser(this.state.userToInvite)) { sections.push(({ undefined, data: [this.state.userToInvite], shouldShow: true, - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, })); } @@ -227,7 +226,6 @@ class IOUParticipantsSplit extends Component { shouldShowConfirmButton confirmButtonText={this.props.translate('common.next')} onConfirmSelection={this.finalizeParticipants} - shouldDelayFocus /> diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index 5c26bb2905a1..bc233b305676 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -32,7 +32,7 @@ export default PropTypes.shape({ lastMessageText: PropTypes.string, /** The time of the last message on the report */ - lastMessageTimestamp: PropTypes.number, + lastActionCreated: PropTypes.string, /** The sequence number of the last action read by the user */ lastReadSequenceNumber: PropTypes.number, diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 994ed5e099c7..c2bf6b384b87 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; +import {withNetwork} from '../../components/OnyxProvider'; import styles from '../../styles/styles'; import Text from '../../components/Text'; import * as Session from '../../libs/actions/Session'; @@ -28,6 +29,7 @@ import cardPropTypes from '../../components/cardPropTypes'; import * as Wallet from '../../libs/actions/Wallet'; import walletTermsPropTypes from '../EnablePayments/walletTermsPropTypes'; import * as PolicyUtils from '../../libs/PolicyUtils'; +import ConfirmModal from '../../components/ConfirmModal'; const propTypes = { /* Onyx Props */ @@ -96,6 +98,12 @@ class InitialSettingsPage extends React.Component { this.getWalletBalance = this.getWalletBalance.bind(this); this.getDefaultMenuItems = this.getDefaultMenuItems.bind(this); this.getMenuItem = this.getMenuItem.bind(this); + this.toggleSignoutConfirmModal = this.toggleSignoutConfirmModal.bind(this); + this.signout = this.signOut.bind(this); + + this.state = { + shouldShowSignoutConfirmModal: false, + }; } componentDidMount() { @@ -171,7 +179,7 @@ class InitialSettingsPage extends React.Component { { translationKey: 'initialSettingsPage.signOut', icon: Expensicons.Exit, - action: Session.signOutAndRedirectToSignIn, + action: () => { this.signout(false); }, }, ]); } @@ -199,6 +207,20 @@ class InitialSettingsPage extends React.Component { ); } + toggleSignoutConfirmModal(value) { + this.setState({shouldShowSignoutConfirmModal: value}); + } + + signOut(shouldForceSignout = false) { + if (!this.props.network.isOffline || shouldForceSignout) { + Session.signOutAndRedirectToSignIn(); + return; + } + + // When offline, warn the user that any actions they took while offline will be lost if they sign out + this.toggleSignoutConfirmModal(true); + } + openProfileSettings() { Navigation.navigate(ROUTES.SETTINGS_PROFILE); } @@ -247,6 +269,17 @@ class InitialSettingsPage extends React.Component { )} {_.map(this.getDefaultMenuItems(), (item, index) => this.getMenuItem(item, index))} + + this.signOut(true)} + onCancel={() => this.toggleSignoutConfirmModal(false)} + /> @@ -286,4 +319,5 @@ export default compose( key: ONYXKEYS.WALLET_TERMS, }, }), + withNetwork(), )(InitialSettingsPage); diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js index 11986e260909..6de7b44bf259 100644 --- a/src/pages/settings/Payments/AddDebitCardPage.js +++ b/src/pages/settings/Payments/AddDebitCardPage.js @@ -164,6 +164,7 @@ class DebitCardPage extends Component { label={this.props.translate('common.zip')} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} + hint={this.props.translate('common.zipCodeExample')} /> diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js index 7691b02e9f43..b1be756e2a17 100644 --- a/src/pages/settings/Payments/PaymentMethodList.js +++ b/src/pages/settings/Payments/PaymentMethodList.js @@ -60,6 +60,9 @@ const propTypes = { /** ID of selected payment method */ selectedMethodID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + /** Content for the FlatList header component */ + listHeaderComponent: PropTypes.func, + ...withLocalizePropTypes, }; @@ -76,6 +79,7 @@ const defaultProps = { actionPaymentMethodType: '', activePaymentMethodID: '', selectedMethodID: '', + listHeaderComponent: null, }; class PaymentMethodList extends Component { @@ -215,6 +219,7 @@ class PaymentMethodList extends Component { renderItem={this.renderItem} keyExtractor={item => item.key} ListEmptyComponent={this.renderListEmptyComponent()} + ListHeaderComponent={this.props.listHeaderComponent} /> { this.props.shouldShowAddPaymentMethodButton diff --git a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js index a0df14a8d856..b308898e4662 100644 --- a/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js +++ b/src/pages/settings/Payments/PaymentsPage/BasePaymentsPage.js @@ -63,6 +63,7 @@ class BasePaymentsPage extends React.Component { this.hidePasswordPrompt = this.hidePasswordPrompt.bind(this); this.navigateToTransferBalancePage = this.navigateToTransferBalancePage.bind(this); this.setMenuPosition = this.setMenuPosition.bind(this); + this.listHeaderComponent = this.listHeaderComponent.bind(this); } componentDidMount() { @@ -260,9 +261,57 @@ class BasePaymentsPage extends React.Component { Navigation.navigate(ROUTES.SETTINGS_PAYMENTS_TRANSFER_BALANCE); } + listHeaderComponent() { + return ( + <> + {Permissions.canUseWallet(this.props.betas) && ( + <> + + + + + + {this.props.userWallet.currentBalance > 0 && ( + + {triggerKYCFlow => ( + + )} + + )} + + )} + + {this.props.translate('paymentsPage.paymentMethodsTitle')} + + + ); + } + render() { const isPayPalMeSelected = this.state.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PAYPAL; - const shouldShowMakeDefaultButton = !this.state.isSelectedPaymentMethodDefault && Permissions.canUseWallet(this.props.betas) && !isPayPalMeSelected; + const shouldShowMakeDefaultButton = !this.state.isSelectedPaymentMethodDefault + && Permissions.canUseWallet(this.props.betas) + && !isPayPalMeSelected + && !(this.state.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT && this.state.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS); // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web or Desktop screens const isPopoverBottomMount = this.state.anchorPositionTop === 0 || this.props.isSmallScreenWidth; @@ -275,44 +324,6 @@ class BasePaymentsPage extends React.Component { onCloseButtonPress={() => Navigation.dismissModal(true)} /> - {Permissions.canUseWallet(this.props.betas) && ( - <> - - - - - - {this.props.userWallet.currentBalance > 0 && ( - - {triggerKYCFlow => ( - - )} - - )} - - )} - - {this.props.translate('paymentsPage.paymentMethodsTitle')} - diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js index 84cc1db5df1d..6c805ade9b17 100755 --- a/src/pages/settings/Profile/ProfilePage.js +++ b/src/pages/settings/Profile/ProfilePage.js @@ -28,30 +28,22 @@ import * as ValidationUtils from '../../../libs/ValidationUtils'; import * as ReportUtils from '../../../libs/ReportUtils'; import Form from '../../../components/Form'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; -import * as LoginUtils from '../../../libs/LoginUtils'; - -const loginPropTypes = PropTypes.shape({ - /** Value of partner name */ - partnerName: PropTypes.string, - - /** Phone/Email associated with user */ - partnerUserID: PropTypes.string, - - /** Date of when login was validated */ - validatedDate: PropTypes.string, -}); const propTypes = { /* Onyx Props */ /** Login list for the user that is signed in */ - loginList: PropTypes.oneOfType([ - PropTypes.objectOf(loginPropTypes), + loginList: PropTypes.shape({ + /** Value of partner name */ + partnerName: PropTypes.string, + + /** Phone/Email associated with user */ + partnerUserID: PropTypes.string, + + /** Date of when login was validated */ + validatedDate: PropTypes.string, + }), - // TODO: remove this once this closes: - // https://github.com/Expensify/App/issues/10960 - PropTypes.arrayOf(loginPropTypes), - ]), ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, }; @@ -77,7 +69,7 @@ class ProfilePage extends Component { this.avatar = {uri: lodashGet(this.props.currentUserPersonalDetails, 'avatar') || this.defaultAvatar}; this.pronouns = props.currentUserPersonalDetails.pronouns; this.state = { - logins: this.getLogins(LoginUtils.convertLoginListToObject(props.loginList)), + logins: this.getLogins(), selectedTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'selected', CONST.DEFAULT_TIME_ZONE.selected), isAutomaticTimezone: lodashGet(props.currentUserPersonalDetails.timezone, 'automatic', CONST.DEFAULT_TIME_ZONE.automatic), hasSelfSelectedPronouns: !_.isEmpty(props.currentUserPersonalDetails.pronouns) && !props.currentUserPersonalDetails.pronouns.startsWith(CONST.PRONOUNS.PREFIX), @@ -94,10 +86,8 @@ class ProfilePage extends Component { let stateToUpdate = {}; // Recalculate logins if loginList has changed - const currentLoginList = LoginUtils.convertLoginListToObject(this.props.loginList); - const prevLoginList = LoginUtils.convertLoginListToObject(prevProps.loginList); - if (_.keys(currentLoginList).length !== _.keys(prevLoginList).length) { - stateToUpdate = {...stateToUpdate, logins: this.getLogins(currentLoginList)}; + if (_.keys(this.props.loginList).length !== _.keys(prevProps.loginList).length) { + stateToUpdate = {...stateToUpdate, logins: this.getLogins()}; } if (_.isEmpty(stateToUpdate)) { @@ -141,11 +131,10 @@ class ProfilePage extends Component { /** * Get the most validated login of each type * - * @param {Object} loginList * @returns {Object} */ - getLogins(loginList) { - return _.reduce(_.values(loginList), (logins, currentLogin) => { + getLogins() { + return _.reduce(_.values(this.props.loginList), (logins, currentLogin) => { const type = Str.isSMSLogin(currentLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; const login = Str.removeSMSDomain(currentLogin.partnerUserID); diff --git a/src/pages/signin/LoginForm.js b/src/pages/signin/LoginForm.js index 2e47f6b39b14..e9813c3bfb1f 100755 --- a/src/pages/signin/LoginForm.js +++ b/src/pages/signin/LoginForm.js @@ -12,7 +12,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/ import compose from '../../libs/compose'; import canFocusInputOnScreenFocus from '../../libs/canFocusInputOnScreenFocus'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; -import getEmailKeyboardType from '../../libs/getEmailKeyboardType'; import TextInput from '../../components/TextInput'; import * as ValidationUtils from '../../libs/ValidationUtils'; import * as LoginUtils from '../../libs/LoginUtils'; @@ -24,6 +23,7 @@ import networkPropTypes from '../../components/networkPropTypes'; import * as ErrorUtils from '../../libs/ErrorUtils'; import DotIndicatorMessage from '../../components/DotIndicatorMessage'; import * as CloseAccount from '../../libs/actions/CloseAccount'; +import CONST from '../../CONST'; const propTypes = { /** Should we dismiss the keyboard when transitioning away from the page? */ @@ -177,7 +177,7 @@ class LoginForm extends React.Component { onSubmitEditing={this.validateAndSubmitForm} autoCapitalize="none" autoCorrect={false} - keyboardType={getEmailKeyboardType()} + keyboardType={CONST.KEYBOARD_TYPE.EMAIL_ADDRESS} /> {!_.isEmpty(this.props.account.success) && ( diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 395707142f7a..fbabe111ac26 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -132,12 +132,15 @@ class WorkspaceInvitePage extends React.Component { */ getSections() { const sections = []; + let indexOffset = 0; + sections.push({ title: undefined, data: this.state.selectedOptions, shouldShow: true, - indexOffset: 0, + indexOffset, }); + indexOffset += this.state.selectedOptions.length; // Filtering out selected users from the search results const filterText = _.reduce(this.state.selectedOptions, (str, {login}) => `${str} ${login}`, ''); @@ -148,15 +151,16 @@ class WorkspaceInvitePage extends React.Component { title: this.props.translate('common.contacts'), data: personalDetailsWithoutSelected, shouldShow: !_.isEmpty(personalDetailsWithoutSelected), - indexOffset: _.reduce(sections, (prev, {data}) => prev + data.length, 0), + indexOffset, }); + indexOffset += personalDetailsWithoutSelected.length; if (hasUnselectedUserToInvite) { sections.push(({ title: undefined, data: [this.state.userToInvite], shouldShow: true, - indexOffset: 0, + indexOffset, })); } diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 67091d53c92c..6ff197f4f55e 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -152,9 +152,11 @@ class WorkspaceMembersPage extends React.Component { * Toggle user from the selectedEmployees list * * @param {String} login + * @param {String} pendingAction + * */ - toggleUser(login) { - if (this.willTooltipShowForLogin(login)) { + toggleUser(login, pendingAction) { + if (this.willTooltipShowForLogin(login) || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } @@ -250,26 +252,26 @@ class WorkspaceMembersPage extends React.Component { renderItem({ item, }) { - const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login; + const canBeRemoved = this.props.policy.owner !== item.login && this.props.session.email !== item.login && item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; return ( this.dismissError(item)} pendingAction={item.pendingAction} errors={item.errors}> this.willTooltipShowForLogin(item.login, true)} onHoverOut={() => this.setState({showTooltipForLogin: ''})}> this.toggleUser(item.login)} + onPress={() => this.toggleUser(item.login, item.pendingAction)} activeOpacity={0.7} > this.toggleUser(item.login)} + onPress={() => this.toggleUser(item.login, item.pendingAction)} toggleTooltip={this.state.showTooltipForLogin === item.login} text={this.props.translate('workspace.people.error.cannotRemove')} /> this.toggleUser(item.login)} + onSelectRow={() => this.toggleUser(item.login, item.pendingAction)} forceTextUnreadStyle isDisabled={!canBeRemoved} option={{ @@ -301,8 +303,7 @@ class WorkspaceMembersPage extends React.Component { const removableMembers = []; let data = []; _.each(policyMemberList, (policyMember, email) => { - if (policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } - if (email !== this.props.session.email && email !== this.props.policy.owner) { + 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}); diff --git a/src/pages/workspace/WorkspacesListPage.js b/src/pages/workspace/WorkspacesListPage.js index e75bfe2dead8..41c6b3babed3 100755 --- a/src/pages/workspace/WorkspacesListPage.js +++ b/src/pages/workspace/WorkspacesListPage.js @@ -122,7 +122,7 @@ class WorkspacesListPage extends Component { iconType: policy.avatar ? CONST.ICON_TYPE_AVATAR : CONST.ICON_TYPE_ICON, action: () => Navigation.navigate(ROUTES.getWorkspaceInitialRoute(policy.id)), iconStyles: policy.avatar ? [] : [styles.popoverMenuIconEmphasized], - iconFill: themeColors.iconReversed, + iconFill: themeColors.textLight, fallbackIcon: Expensicons.FallbackWorkspaceAvatar, brickRoadIndicator: PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, this.props.policyMembers), pendingAction: policy.pendingAction, diff --git a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js b/src/pages/workspace/bills/WorkspaceBillsFirstSection.js index 85f1ecac7565..f283ec9d8469 100644 --- a/src/pages/workspace/bills/WorkspaceBillsFirstSection.js +++ b/src/pages/workspace/bills/WorkspaceBillsFirstSection.js @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import Text from '../../../components/Text'; import styles from '../../../styles/styles'; +import themeColors from '../../../styles/themes/default'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import * as Expensicons from '../../../components/Icon/Expensicons'; import * as Illustrations from '../../../components/Icon/Illustrations'; @@ -38,7 +39,7 @@ const WorkspaceBillsFirstSection = (props) => { return (
{ icon: Expensicons.Bill, shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, + iconFill: themeColors.success, + wrapperStyle: [styles.cardMenuItem], }, ]} + containerStyles={[styles.cardSection]} > - + {props.translate('workspace.bills.askYourVendorsBeforeEmail')} {props.user.isFromPublicDomain ? ( diff --git a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js index 001ecf0273ba..3e5449d92921 100644 --- a/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js +++ b/src/pages/workspace/bills/WorkspaceBillsNoVBAView.js @@ -24,9 +24,10 @@ const WorkspaceBillsNoVBAView = props => (
- + {props.translate('workspace.bills.unlockNoVBACopy')}