diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f4f6a90ae6db..1446f1e4d851 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -94,7 +94,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I followed the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md) - [ ] I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` are working as expected) - [ ] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests) -- [ ] I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such +- [ ] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such - [ ] I verified that if a function's arguments changed that all usages have also been updated correctly - [ ] If any new file was added I verified that: - [ ] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory @@ -109,6 +109,7 @@ This is a checklist for PR authors. Please make sure to complete all tasks and c - [ ] I verified that all the inputs inside a form are aligned with each other. - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. +- [ ] I added [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. ### Screenshots/Videos diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1ceb12a30af5..2cacdf557560 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -114,51 +114,6 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - submitAndroid: - name: Submit Android app for production review - needs: prep - if: ${{ github.ref == 'refs/heads/production' }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Ruby - uses: ruby/setup-ruby@v1.190.0 - with: - bundler-cache: true - - - name: Get Android native version - id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -o 'versionCode\s\+[0-9]\+' android/app/build.gradle | awk '{ print $2 }')" >> "$GITHUB_OUTPUT" - - - name: Decrypt json w/ Google Play credentials - run: gpg --batch --yes --decrypt --passphrase="${{ secrets.LARGE_SECRET_PASSPHRASE }}" --output android-fastlane-json-key.json android-fastlane-json-key.json.gpg - working-directory: android/app - - - name: Submit Android build for review - run: bundle exec fastlane android upload_google_play_production - env: - VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - - - name: Warn deployers if Android production deploy failed - if: ${{ failure() }} - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - channel: '#deployer', - attachments: [{ - color: "#DB4545", - pretext: ``, - text: `💥 Android production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . 💥`, - }] - } - env: - GITHUB_TOKEN: ${{ github.token }} - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} - android_hybrid: name: Build and deploy Android HybridApp needs: prep @@ -431,12 +386,6 @@ jobs: APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} - - name: Submit build for App Store review - if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios submit_for_review - env: - VERSION: ${{ steps.getIOSVersion.outputs.IOS_VERSION }} - - name: Upload iOS build to Browser Stack if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} run: curl -u "$BROWSERSTACK" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@/Users/runner/work/App/App/New Expensify.ipa" @@ -730,7 +679,7 @@ jobs: name: Post a Slack message when any platform fails to build or deploy runs-on: ubuntu-latest if: ${{ failure() }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -745,21 +694,15 @@ jobs: outputs: IS_AT_LEAST_ONE_PLATFORM_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAtLeastOnePlatform.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED }} IS_ALL_PLATFORMS_DEPLOYED: ${{ steps.checkDeploymentSuccessOnAllPlatforms.outputs.IS_ALL_PLATFORMS_DEPLOYED }} - needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] + needs: [buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform id: checkDeploymentSuccessOnAtLeastOnePlatform run: | isAtLeastOnePlatformDeployed="false" - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi - else - if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then - isAtLeastOnePlatformDeployed="true" - fi + if [ "${{ needs.uploadAndroid.result }}" == "success" ]; then + isAtLeastOnePlatformDeployed="true" fi if [ "${{ needs.iOS.result }}" == "success" ] || \ @@ -784,14 +727,8 @@ jobs: isAllPlatformsDeployed="true" fi - if [ ${{ github.ref }} == 'refs/heads/production' ]; then - if [ "${{ needs.submitAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi - else - if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then - isAllPlatformsDeployed="false" - fi + if [ "${{ needs.uploadAndroid.result }}" != "success" ]; then + isAllPlatformsDeployed="false" fi echo "IS_ALL_PLATFORMS_DEPLOYED=$isAllPlatformsDeployed" >> "$GITHUB_OUTPUT" @@ -939,7 +876,7 @@ jobs: name: Post a Slack message when all platforms deploy successfully runs-on: ubuntu-latest if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_ALL_PLATFORMS_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] steps: - name: 'Announces the deploy in the #announce Slack room' uses: 8398a7/action-slack@v3 @@ -993,11 +930,11 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] with: version: ${{ needs.prep.outputs.APP_VERSION }} env: ${{ github.ref == 'refs/heads/production' && 'production' || 'staging' }} - android: ${{ github.ref == 'refs/heads/production' && needs.submitAndroid.result || needs.uploadAndroid.result }} + android: ${{ github.ref == 'refs/heads/production' && needs.uploadAndroid.result }} android_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} ios_hybrid: ${{ needs.iOS_hybrid.result }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 5d10e3d6f6f8..672776f6ecb4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006606 - versionName "9.0.66-6" + versionCode 1009006607 + versionName "9.0.66-7" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/contributingGuides/PROPOSAL_TEMPLATE.md b/contributingGuides/PROPOSAL_TEMPLATE.md index 8c9fa7968fe2..ea908b5b0666 100644 --- a/contributingGuides/PROPOSAL_TEMPLATE.md +++ b/contributingGuides/PROPOSAL_TEMPLATE.md @@ -7,6 +7,9 @@ ### What changes do you think we should make in order to solve the problem? +### What specific scenarios should we cover in unit tests to prevent reintroducing this issue in the future? + + ### What alternative solutions did you explore? (Optional) **Reminder:** Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job. diff --git a/contributingGuides/REVIEWER_CHECKLIST.md b/contributingGuides/REVIEWER_CHECKLIST.md index 5fc14328f3b4..545c79a95af1 100644 --- a/contributingGuides/REVIEWER_CHECKLIST.md +++ b/contributingGuides/REVIEWER_CHECKLIST.md @@ -30,7 +30,7 @@ - [ ] I verified that this PR follows the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md) - [ ] I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` have been tested & I retested again) - [ ] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests) -- [ ] I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such +- [ ] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such - [ ] If a new component is created I verified that: - [ ] A similar component doesn't exist in the codebase - [ ] All props are defined accurately and each prop has a `/** comment above it */` @@ -54,6 +54,7 @@ - [ ] I verified that all the inputs inside a form are aligned with each other. - [ ] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes. - [ ] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page. +- [ ] For any bug fix or new feature in this PR, I verified that sufficient [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) are included to prevent regressions in this flow. - [ ] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps. - [ ] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR. diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index cd38fcaaaf6c..cd2598608a0f 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -638,6 +638,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", @@ -658,6 +659,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", @@ -842,6 +844,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_ROOT}/../../node_modules/@expensify/react-native-live-markdown/parser/react-native-live-markdown-parser.js", + "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", @@ -862,6 +865,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/react-native-live-markdown-parser.js", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 2cd9c81c19ca..45fe3eb36805 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.66.6 + 9.0.66.7 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 57ba616450b6..05f70824981c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.66.6 + 9.0.66.7 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 27a481ab98ef..eb799cfd6323 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.66 CFBundleVersion - 9.0.66.6 + 9.0.66.7 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5ea5b19896e4..21633b432c12 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1722,7 +1722,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-keyboard-controller (1.14.1): + - react-native-keyboard-controller (1.14.4): - DoubleConversion - glog - hermes-engine @@ -2391,7 +2391,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.183): + - RNLiveMarkdown (0.1.187): - DoubleConversion - glog - hermes-engine @@ -2411,9 +2411,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.183) + - RNLiveMarkdown/newarch (= 0.1.187) - Yoga - - RNLiveMarkdown/newarch (0.1.183): + - RNLiveMarkdown/newarch (0.1.187): - DoubleConversion - glog - hermes-engine @@ -3236,7 +3236,7 @@ SPEC CHECKSUMS: react-native-geolocation: b9bd12beaf0ebca61a01514517ca8455bd26fa06 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 react-native-key-command: aae312752fcdfaa2240be9a015fc41ce54087546 - react-native-keyboard-controller: 902c07f41a415b632583b384427a71770a8b02a3 + react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 react-native-pager-view: c64a744211a46202619a77509f802765d1659dba @@ -3286,7 +3286,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: fa9c6451960d09209bb5698745a0a66330ec53cc + RNLiveMarkdown: 8338447b39fcd86596c74b9e0e9509e365a2dd3b RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/package-lock.json b/package-lock.json index a58e33023eef..8749de85e87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.66-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.66-7", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.183", + "@expensify/react-native-live-markdown": "0.1.187", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3632,14 +3632,14 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.183", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.183.tgz", - "integrity": "sha512-egxknos7ghe4M5Z2rK7DvphcaxQBdxyppu5N2tdCVc/3oPO2ZtBNjDjtksqywC12wPtIYgHSgxrzvLEfbh5skw==", + "version": "0.1.187", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.187.tgz", + "integrity": "sha512-bw+dfhRN31u2xfG8LCI3e28g5EG/BfkyX1EqjPBRQlDZo4fZsdA61UFW6P8Y4rHlqspjYXJ0vk4ctECRWYl4Yg==", "license": "MIT", "workspaces": [ - "parser", - "example", - "WebExample" + "./parser", + "./example", + "./WebExample" ], "engines": { "node": ">= 18.0.0" diff --git a/package.json b/package.json index c82882a2c9cf..60f04d59605a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.66-6", + "version": "9.0.66-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -68,7 +68,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.183", + "@expensify/react-native-live-markdown": "0.1.187", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", diff --git a/src/CONST.ts b/src/CONST.ts index ee70e3b29668..7afdc412ddb9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3815,8 +3815,8 @@ const CONST = { }, GA: {}, GB: { - regex: /^[A-Z]{1,2}[0-9R][0-9A-Z]?\s*[0-9][A-Z-CIKMOV]{2}$/, - samples: 'LA102UX, BL2F8FX, BD1S9LU, WR4G 6LH', + regex: /^[A-Z]{1,2}[0-9R][0-9A-Z]?\s*([0-9][ABD-HJLNP-UW-Z]{2})?$/, + samples: 'LA102UX, BL2F8FX, BD1S9LU, WR4G 6LH, W1U', }, GD: {}, GE: { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 36586b09e514..96bdf8e9e1e8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -4,9 +4,9 @@ import isEmpty from 'lodash/isEmpty'; import React from 'react'; import {StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; import {TNodeChildrenRenderer} from 'react-native-render-html'; -import {usePersonalDetails} from '@components/OnyxProvider'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; @@ -20,6 +20,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import asMutable from '@src/types/utils/asMutable'; @@ -31,7 +32,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const htmlAttribAccountID = tnode.attributes.accountid; - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const htmlAttributeAccountID = tnode.attributes.accountid; let accountID: number; @@ -56,7 +57,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona return displayText.split('@').at(0); }; - if (!isEmpty(htmlAttribAccountID)) { + if (!isEmpty(htmlAttribAccountID) && personalDetails?.[htmlAttribAccountID]) { const user = personalDetails[htmlAttribAccountID]; accountID = parseInt(htmlAttribAccountID, 10); mentionDisplayText = LocalePhoneNumber.formatPhoneNumber(user?.login ?? '') || PersonalDetailsUtils.getDisplayNameOrDefault(user); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d01b69ed5649..83636ef38828 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -120,10 +120,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction); const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID); const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason}); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), - [moneyRequestReport, chatReport, policy, transaction], + (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, transactionViolations, onlyShowPayElsewhere), + [moneyRequestReport, chatReport, policy, transaction, transactionViolations], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); @@ -135,7 +136,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]); + const shouldShowApproveButton = useMemo( + () => IOU.canApproveIOU(moneyRequestReport, policy, transactionViolations) && !hasOnlyPendingTransactions, + [moneyRequestReport, policy, hasOnlyPendingTransactions, transactionViolations], + ); const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5edeffd4dea4..0dbff0fe18e1 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -330,14 +330,14 @@ function ReportPreview({ const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere), - [iouReport, chatReport, policy, allTransactions], + (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, transactionViolations, onlyShowPayElsewhere), + [iouReport, chatReport, policy, allTransactions, transactionViolations], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]); + const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy, transactionViolations), [iouReport, policy, transactionViolations]); const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 3c38c9f4c4a3..1e3ce6119315 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -45,6 +45,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy); const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); + const canEditReportDescription = ReportUtils.canEditReportDescription(report, policy); const filteredOptions = moneyRequestOptions.filter( (item): item is Exclude => item !== CONST.IOU.TYPE.INVOICE, @@ -123,12 +124,13 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { (welcomeMessage?.messageHtml ? ( { - if (!canEditPolicyDescription) { + if (!canEditReportDescription) { return; } - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1')); + const activeRoute = Navigation.getActiveRoute(); + Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1', activeRoute)); }} - style={[styles.renderHTML, canEditPolicyDescription ? styles.cursorPointer : styles.cursorText]} + style={[styles.renderHTML, canEditReportDescription ? styles.cursorPointer : styles.cursorText]} accessibilityLabel={translate('reportDescriptionPage.roomDescription')} > @@ -153,8 +155,8 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { (welcomeMessage?.messageHtml ? ( { - const activeRoute = Navigation.getReportRHPActiveRoute(); - if (ReportUtils.canEditReportDescription(report, policy)) { + const activeRoute = Navigation.getActiveRoute(); + if (canEditReportDescription) { Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1', activeRoute)); return; } diff --git a/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts b/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts index 0ab82ba6b755..7f57658f2016 100644 --- a/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts +++ b/src/libs/API/parameters/SetPersonalDetailsAndShipExpensifyCardsParams.ts @@ -9,6 +9,7 @@ type SetPersonalDetailsAndShipExpensifyCardsParams = { addressCountry: string; addressState: string; dob: string; + validateCode: string; }; export default SetPersonalDetailsAndShipExpensifyCardsParams; diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 671fb03f268b..aa87f28bef4b 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -480,7 +480,6 @@ function validateReportDraftProperty(key: keyof Report, value: string) { case 'isOptimisticReport': case 'isWaitingOnBankAccount': case 'isCancelledIOU': - case 'isHidden': return validateBoolean(value); case 'lastReadSequenceNumber': case 'managerID': @@ -621,7 +620,6 @@ function validateReportDraftProperty(key: keyof Report, value: string) { iouReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, preexistingReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, nonReimbursableTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION, - isHidden: CONST.RED_BRICK_ROAD_PENDING_ACTION, pendingChatMembers: CONST.RED_BRICK_ROAD_PENDING_ACTION, fieldList: CONST.RED_BRICK_ROAD_PENDING_ACTION, permissions: CONST.RED_BRICK_ROAD_PENDING_ACTION, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 952e0c2fe4cc..36095b7d88ec 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -329,7 +329,6 @@ type OptimisticChatReport = Pick< | 'writeCapability' | 'avatarUrl' | 'invoiceReceiver' - | 'isHidden' > & { isOptimisticReport: true; }; @@ -6518,8 +6517,6 @@ function reasonForReportToBeInOptionList({ !report?.reportID || !report?.type || report?.reportName === undefined || - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - report?.isHidden || (!report?.participants && // We omit sending back participants for chat rooms when searching for reports since they aren't needed to display the results and can get very large. // So we allow showing rooms with no participants–in any other circumstances we should never have these reports with no participants in Onyx. @@ -7400,7 +7397,6 @@ function getTaskAssigneeChatOnyxData( pendingFields: { createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, - isHidden: false, }, }, { diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e100fb885fff..489502b1de9d 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -289,7 +289,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr const chatReportRNVP = data[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.chatReportID}`] ?? undefined; if ( - IOU.canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy) && + IOU.canIOUBePaid(report, chatReport, policy, allReportTransactions, undefined, false, chatReportRNVP, invoiceReceiverPolicy) && !ReportUtils.hasOnlyHeldExpenses(report.reportID, allReportTransactions) ) { return CONST.SEARCH.ACTION_TYPES.PAY; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d6724ab89b41..3fb01f916acd 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6812,6 +6812,7 @@ function sendMoneyWithWallet(report: OnyxEntry, amount: number function canApproveIOU( iouReport: OnyxTypes.OnyxInputOrEntry | SearchReport, policy: OnyxTypes.OnyxInputOrEntry | SearchPolicy, + violations?: OnyxCollection, chatReportRNVP?: OnyxTypes.ReportNameValuePairs, ) { // Only expense reports can be approved @@ -6832,6 +6833,8 @@ function canApproveIOU( const iouSettled = ReportUtils.isSettled(iouReport?.reportID); const reportNameValuePairs = chatReportRNVP ?? ReportUtils.getReportNameValuePairs(iouReport?.reportID); const isArchivedReport = ReportUtils.isArchivedRoom(iouReport, reportNameValuePairs); + const allViolations = violations ?? allTransactionViolations; + const hasViolations = ReportUtils.hasViolations(iouReport?.reportID ?? '-1', allViolations); let isTransactionBeingScanned = false; const reportTransactions = TransactionUtils.getAllReportTransactions(iouReport?.reportID); for (const transaction of reportTransactions) { @@ -6844,7 +6847,7 @@ function canApproveIOU( } } - return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedReport && !isTransactionBeingScanned; + return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedReport && !isTransactionBeingScanned && !hasViolations; } function canIOUBePaid( @@ -6852,6 +6855,7 @@ function canIOUBePaid( chatReport: OnyxTypes.OnyxInputOrEntry | SearchReport, policy: OnyxTypes.OnyxInputOrEntry | SearchPolicy, transactions?: OnyxTypes.Transaction[] | SearchTransaction[], + violations?: OnyxCollection, onlyShowPayElsewhere = false, chatReportRNVP?: OnyxTypes.ReportNameValuePairs, invoiceReceiverPolicy?: SearchPolicy, @@ -6898,7 +6902,9 @@ function canIOUBePaid( const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); const isAutoReimbursable = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES ? false : ReportUtils.canBeAutoReimbursed(iouReport, policy); - const shouldBeApproved = canApproveIOU(iouReport, policy); + const allViolations = violations ?? allTransactionViolations; + const shouldBeApproved = canApproveIOU(iouReport, policy, allViolations); + const hasViolations = ReportUtils.hasViolations(iouReport?.reportID ?? '-1', allViolations); const isPayAtEndExpenseReport = ReportUtils.isPayAtEndExpenseReport(iouReport?.reportID, transactions); return ( @@ -6910,6 +6916,7 @@ function canIOUBePaid( !isChatReportArchived && !isAutoReimbursable && !shouldBeApproved && + !hasViolations && !isPayAtEndExpenseReport ); } @@ -6920,7 +6927,7 @@ function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry { const iouReport = ReportUtils.getReportOrDraftReport(action.childReportID ?? '-1'); const policy = PolicyUtils.getPolicy(iouReport?.policyID); - const shouldShowSettlementButton = canIOUBePaid(iouReport, chatReport, policy) || canApproveIOU(iouReport, policy); + const shouldShowSettlementButton = canIOUBePaid(iouReport, chatReport, policy, undefined, allTransactionViolations) || canApproveIOU(iouReport, policy, allTransactionViolations); return action.childReportID?.toString() !== excludedIOUReportID && action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton; }); } @@ -7196,6 +7203,30 @@ function unapproveExpenseReport(expenseReport: OnyxEntry) { }, ]; + if (expenseReport.parentReportID && expenseReport.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + childStatusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: expenseReport.stateNum, + childStatusNum: expenseReport.statusNum, + }, + }, + }); + } + const parameters: UnapproveExpenseReportParams = { reportID: expenseReport.reportID, reportActionID: optimisticUnapprovedReportAction.reportActionID, @@ -7406,6 +7437,30 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O }, ]; + if (expenseReport.parentReportID && expenseReport.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: stateNum, + childStatusNum: statusNum, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.parentReportID}`, + value: { + [expenseReport.parentReportActionID]: { + childStateNum: expenseReport.stateNum, + childStatusNum: expenseReport.statusNum, + }, + }, + }); + } + if (chatReport?.reportID) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index f759decda812..94a9dc95e846 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -465,7 +465,7 @@ function clearAvatarErrors() { }); } -function updatePersonalDetailsAndShipExpensifyCards(values: FormOnyxValues) { +function updatePersonalDetailsAndShipExpensifyCards(values: FormOnyxValues, validateCode: string) { const parameters: SetPersonalDetailsAndShipExpensifyCardsParams = { legalFirstName: values.legalFirstName?.trim() ?? '', legalLastName: values.legalLastName?.trim() ?? '', @@ -477,6 +477,7 @@ function updatePersonalDetailsAndShipExpensifyCards(values: FormOnyxValues = useRef(null); const values = useMemo(() => getSubstepValues(privatePersonalDetails, draftValues), [privatePersonalDetails, draftValues]); @@ -44,9 +52,7 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi if (!values) { return; } - PersonalDetails.updatePersonalDetailsAndShipExpensifyCards(values); - FormActions.clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); - Navigation.goBack(); + setIsValidateCodeActionModalVisible(true); }, [values]); const { @@ -75,6 +81,23 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi prevScreen(); }; + const handleValidateCodeEntered = useCallback( + (validateCode: string) => { + PersonalDetails.updatePersonalDetailsAndShipExpensifyCards(values, validateCode); + FormActions.clearDraftValues(ONYXKEYS.FORMS.PERSONAL_DETAILS_FORM); + Navigation.goBack(); + }, + [values], + ); + + const sendValidateCode = () => { + if (validateCodeAction?.validateCodeSent) { + return; + } + + requestValidateCodeAction(); + }; + const handleNextScreen = useCallback(() => { if (isEditing) { goToTheLastStep(); @@ -108,6 +131,17 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi screenIndex={screenIndex} personalDetailsValues={values} /> + + {}} + onClose={() => setIsValidateCodeActionModalVisible(false)} + isVisible={isValidateCodeActionModalVisible} + title={translate('cardPage.validateCardTitle')} + descriptionPrimary={translate('cardPage.enterMagicCode', {contactMethod: primaryLogin})} + hasMagicCodeBeenSent={!!validateCodeAction?.validateCodeSent} + /> ); } diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index 6d74ccb46e21..6658e05c298d 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -244,7 +244,10 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) { const shouldShowSavedSearchesMenuItemTitle = Object.values(savedSearches ?? {}).filter((s) => s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length > 0; return ( - <> + {typeMenuItems.map((item, index) => { const onPress = singleExecution(() => { @@ -272,16 +275,11 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) { {shouldShowSavedSearchesMenuItemTitle && ( <> {translate('search.savedSearchesMenuItemTitle')} - - {renderSavedSearchesSection(savedSearchesMenuItems())} - + {renderSavedSearchesSection(savedSearchesMenuItems())} )} - + ); } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 1fb6558cc4da..930f614d606a 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -313,9 +313,22 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { } } }, [hideDefaultDeleteMenu, paymentMethod.methodID, paymentMethod.selectedPaymentMethodType, bankAccountList, fundList, shouldShowDefaultDeleteMenu]); + // Don't show "Make default payment method" button if it's the only payment method or if it's already the default + const isCurrentPaymentMethodDefault = () => { + const hasMultiplePaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, fundList ?? {}, styles).length > 1; + if (hasMultiplePaymentMethods) { + if (paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + return paymentMethod.selectedPaymentMethod.bankAccountID === userWallet?.walletLinkedAccountID; + } + if (paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.DEBIT_CARD) { + return paymentMethod.selectedPaymentMethod.fundID === userWallet?.walletLinkedAccountID; + } + } + return true; + }; const shouldShowMakeDefaultButton = - !paymentMethod.isSelectedPaymentMethodDefault && + !isCurrentPaymentMethodDefault() && !(paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && paymentMethod.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 diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 453be1f58a32..1b26e4950ef4 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -47,7 +47,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { }, [policyID, workspaceAccountID]); const {isOffline} = useNetwork({onReconnect: fetchCompanyCards}); - const isLoading = !isOffline && (!cardFeeds || cardFeeds.isLoading); + const isLoading = !isOffline && (!cardFeeds || (cardFeeds.isLoading && !cardsList)); useFocusEffect(fetchCompanyCards); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 0fec22cd9e34..42be2a8c3613 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -211,9 +211,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** If the report contains nonreimbursable expenses, send the nonreimbursable total */ nonReimbursableTotal?: number; - /** Whether the report is hidden from options list */ - isHidden?: boolean; - /** Collection of participant private notes, indexed by their accountID */ privateNotes?: Record; diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts index 32aa0797d0f8..d556a51cd3de 100644 --- a/src/types/utils/whitelistedReportKeys.ts +++ b/src/types/utils/whitelistedReportKeys.ts @@ -54,7 +54,6 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback< iouReportID: unknown; preexistingReportID: unknown; nonReimbursableTotal: unknown; - isHidden: unknown; privateNotes: unknown; pendingChatMembers: unknown; fieldList: unknown; diff --git a/tests/README.md b/tests/README.md index 890697d80719..08df0b89794b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -59,62 +59,51 @@ expect(onyxData).toBe(expectedOnyxData); ## Documenting Tests -Tests aren't always clear about what exactly is being tested. To make this a bit easier we recommend adopting the following format for code comments: +Comments are just as critical in tests as the tests themselves. They provide context behind why the test was written and what the expected behavior is supposed to be which will benefit the future generations of engineers looking at them. Think about it. When was the last time you saw a unit test and wondered why it was written that way and then you didn't want to touch it because you didn't know if changing the behavior of the test was appropriate or not? It was probably pretty recent :D -``` -// Given -... code that sets initial condition +In order to give future engineers the best context for a unit test, follow this guide: + +1. DO add three sections to every test: + - "Given" - This introduces the initial condition of the test + - "When" - Describes what action is being done and the thing that is being tested + - "Then" - Describes what is expected to happen -// When -... code that does something +2. DO begin each comment section with the literal words "Given", "When", and "Then", just like the examples below. +3. DO explain **WHY** the test is doing what it is doing in every comment. +4. DO NOT explain **WHAT** the code is doing in comments. This information should be self-evident. + +The format looks like this: -// Then -... code that performs the assertion ``` +// BAD +// Given an account +{* code that sets initial condition *} -## Example Test +// When it is closed +{* code that does something *} -```javascript -HttpUtils.xhr = jest.fn(); - -describe('actions/Report', () => { - it('adds an optimistic comment', () => { - // Given an Onyx subscription to a report's `reportActions` - const ACTION_ID = 1; - const REPORT_ID = 1; - let reportActions; - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: val => reportActions = val, - }); - - // Mock Report_AddComment command so it can succeed - HttpUtils.xhr.mockImplementation(() => Promise.resolve({ - jsonCode: 200, - })); - - // When we add a new action to that report - Report.addComment(REPORT_ID, 'Hello!'); - return waitForBatchedUpdates() - .then(() => { - const action = reportActions[ACTION_ID]; - - // Then the action set in the Onyx callback should match - // the comment we left and it will be in a loading state because - // it's an "optimistic comment" - expect(action.message[0].text).toBe('Hello!'); - expect(action.isPending).toBe(true); - }); - }); -}); +// Then the user is logged out +{* code that performs the assertion *} + +// GOOD +// Given an account of a brand new user +{* code that sets initial condition *} + +// When the account is closed by clicking on the close account button +{* code that does something *} + +// Then the user should be logged out because their account is no longer active +{* code that performs the assertion *} ``` ## When to Write a Test -Many of the UI features of our application should go through rigorous testing by you, your PR reviewer, and finally QA before deployment. It's also difficult to maintain UI tests when the UI changes often. Therefore, it's not valuable for us to place every single part of the application UI under test at this time. The manual testing steps should catch most major UI bugs. Therefore, if we are writing any test there should be a **good reason**. +Many of the UI features of our application should go through rigorous testing by you, your PR reviewer, and finally QA before deployment. However, the code is mature enough now that protecting code against regressions is the top priority. **What's a "good reason" to write a test?** +- Any PR that fixes a bug +- When introducing a new feature, cover as much logic as possible by unit tests - Anything that is difficult or impossible to run a manual tests on - e.g. a test to verify an outcome after an authentication token expires (which normally takes two hours) - Areas of the code that are changing often, breaking often, and would benefit from the resiliency an automated test would provide diff --git a/web/index.html b/web/index.html index 1dc0f7836fa4..aaf8bdb621dd 100644 --- a/web/index.html +++ b/web/index.html @@ -44,7 +44,6 @@ } body { overflow: hidden; - overscroll-behavior: none; touch-action: none; } [data-drag-area='true'] {