diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index b0b2d07f990d..29dddbcd3151 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -106,9 +106,6 @@ jobs: runs-on: macos-latest needs: [validateActor, createNewVersion] if: ${{ fromJSON(needs.validateActor.outputs.HAS_WRITE_ACCESS) }} - defaults: - run: - working-directory: Mobile-Expensify steps: - name: Run turnstyle uses: softprops/turnstyle@49108bdfa571e62371bd2c3094893c547ab3fc03 @@ -121,22 +118,17 @@ jobs: uses: actions/checkout@v4 with: ref: main + submodules: true # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify # This is a workaround to allow pushes to a protected branch token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - name: Check out `Mobile-Expensify` repo - uses: actions/checkout@v4 - with: - repository: 'Expensify/Mobile-Expensify' - submodules: true - path: 'Mobile-Expensify' - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Update submodule + - name: Update submodule and checkout the main branch run: | - cd react-native git submodule update --init + cd Mobile-Expensify + git checkout main + git pull origin main - name: Setup git for OSBotify uses: ./.github/actions/composite/setupGitForOSBotify @@ -146,6 +138,7 @@ jobs: - name: Generate HybridApp version run: | + cd Mobile-Expensify # Generate all flavors of the version SHORT_APP_VERSION=$(echo "$NEW_VERSION" | awk -F'-' '{print $1}') BUILD_NUMBER=$(echo "$NEW_VERSION" | awk -F'-' '{print $2}') @@ -178,6 +171,7 @@ jobs: - name: Commit new version run: | + cd Mobile-Expensify git add \ ./Android/AndroidManifest.xml \ ./app/config/config.json \ @@ -186,8 +180,14 @@ jobs: ./iOS/NotificationServiceExtension/Info.plist git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" - - name: Update main branch - run: git push origin main + - name: Update main branch on Mobile-Expensify and App + run: | + cd Mobile-Expensify + git push origin main + cd .. + git add Mobile-Expensify + git commit -m "Update Mobile-Expensify to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + git push origin main - name: Announce failed workflow in Slack if: ${{ failure() }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0f59295a3463..8f12229707ff 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -163,46 +163,25 @@ jobs: name: Build and deploy Android HybridApp needs: prep runs-on: ubuntu-latest-xl - defaults: - run: - working-directory: Mobile-Expensify/react-native steps: - - name: Checkout App repo - uses: actions/checkout@v4 - - - name: Checkout Mobile-Expensify repo + - name: Checkout App and Mobile-Expensify repo uses: actions/checkout@v4 with: - repository: 'Expensify/Mobile-Expensify' submodules: true - path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - - name: Update submodule + - name: Update submodule to match main run: | - git submodule update --init - # Update submodule to latest on staging - git fetch - git checkout staging + git submodule update --init --remote - name: Configure MapBox SDK run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: actions/setup-node@v4 - with: - node-version-file: 'Mobile-Expensify/react-native/.nvmrc' - cache: npm - cache-dependency-path: 'Mobile-Expensify/react-native' - - - name: Install node modules - run: | - npm install - cd .. && npm install - - # Fixes https://github.com/Expensify/App/issues/51682 - npm run grunt:build:shared + - name: Setup Node + id: setup-node + uses: ./.github/actions/composite/setupNode - name: Setup Java uses: actions/setup-java@v4 @@ -214,7 +193,6 @@ jobs: uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - working-directory: 'Mobile-Expensify/react-native' - name: Install New Expensify Gems run: bundle install @@ -229,7 +207,7 @@ jobs: op document get --output ./upload-key.keystore upload-key.keystore op document get --output ./android-fastlane-json-key.json android-fastlane-json-key.json # Copy the keystore to the Android directory for Fullstory - cp ./upload-key.keystore ../Android + cp ./upload-key.keystore Mobile-Expensify/Android - name: Load Android upload keystore credentials from 1Password id: load-credentials @@ -244,7 +222,7 @@ jobs: - name: Get Android native version id: getAndroidVersion - run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' ../Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" + run: echo "VERSION_CODE=$(grep -oP 'android:versionCode="\K[0-9]+' Mobile-Expensify/Android/AndroidManifest.xml)" >> "$GITHUB_OUTPUT" - name: Build Android app if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} @@ -261,10 +239,11 @@ jobs: VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} - name: Get current Android rollout percentage + if: ${{ fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} id: getAndroidRolloutPercentage uses: ./.github/actions/javascript/getAndroidRolloutPercentage with: - GOOGLE_KEY_FILE: Mobile-Expensify/react-native/android-fastlane-json-key.json + GOOGLE_KEY_FILE: ./android-fastlane-json-key.json PACKAGE_NAME: org.me.mobiexpensifyg - name: Submit production build for Google Play review and a slow rollout @@ -496,47 +475,31 @@ jobs: runs-on: macos-13-xlarge env: DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer - defaults: - run: - working-directory: Mobile-Expensify/react-native steps: - name: Checkout uses: actions/checkout@v4 with: - repository: 'Expensify/Mobile-Expensify' submodules: true - path: 'Mobile-Expensify' token: ${{ secrets.OS_BOTIFY_TOKEN }} # fetch-depth: 0 is required in order to fetch the correct submodule branch fetch-depth: 0 - name: Update submodule run: | - git submodule update --init - # Update submodule to latest on staging - git fetch - git checkout staging + git submodule update --init --remote - name: Configure MapBox SDK run: | ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} - - uses: actions/setup-node@v4 + - name: Setup Node id: setup-node - with: - node-version-file: 'Mobile-Expensify/react-native/.nvmrc' - cache-dependency-path: 'Mobile-Expensify/react-native' - - - name: Install node modules - run: | - npm install - cd .. && npm install + uses: ./.github/actions/composite/setupNode - name: Setup Ruby uses: ruby/setup-ruby@v1.190.0 with: bundler-cache: true - working-directory: 'Mobile-Expensify/react-native' - name: Install New Expensify Gems run: bundle install @@ -545,12 +508,12 @@ jobs: uses: actions/cache@v4 id: pods-cache with: - path: ios/Pods - key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} + path: Mobile-Expensify/ios/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('Mobile-Expensify/ios/Podfile.lock', 'firebase.json') }} - name: Compare Podfile.lock and Manifest.lock id: compare-podfile-and-manifest - run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('Mobile-Expensify/ios/Podfile.lock') == hashFiles('Mobile-Expensify/ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" - name: Install cocoapods uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 @@ -558,7 +521,7 @@ jobs: with: timeout_minutes: 10 max_attempts: 5 - command: cd Mobile-Expensify/iOS && pod install + command: npm run pod-install - name: Install 1Password CLI uses: 1password/install-cli-action@v1 diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index 1e79bceb403a..b670e9b9cdb3 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -86,6 +86,7 @@ jobs: androidHybrid: name: Build Android HybridApp needs: [validateActor, getBranchRef] + if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} runs-on: ubuntu-latest-xl defaults: run: @@ -168,10 +169,6 @@ jobs: ANDROID_UPLOAD_KEYSTORE_ALIAS: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_ALIAS ANDROID_UPLOAD_KEY_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEY_PASSWORD - - 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: Build Android app id: build env: @@ -200,11 +197,130 @@ jobs: run: | # $s3APKPath is set from within the Fastfile, android upload_s3 lane echo "S3_APK_PATH=$s3APKPath" >> "$GITHUB_OUTPUT" + + iosHybrid: + name: Build and deploy iOS for testing + needs: [validateActor, getBranchRef] + if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} + env: + DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + runs-on: macos-13-xlarge + defaults: + run: + working-directory: Mobile-Expensify/react-native + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + ref: ${{ env.OLD_DOT_COMMIT }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + # fetch-depth: 0 is required in order to fetch the correct submodule branch + fetch-depth: 0 + + - name: Update submodule + run: | + git submodule update --init + git fetch + git checkout ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }} + + - name: Configure MapBox SDK + run: ./scripts/setup-mapbox-sdk.sh ${{ secrets.MAPBOX_SDK_DOWNLOAD_TOKEN }} + + - uses: actions/setup-node@v4 + id: setup-node + with: + node-version-file: 'Mobile-Expensify/react-native/.nvmrc' + cache: npm + cache-dependency-path: 'Mobile-Expensify/react-native' + + - name: Create .env.adhoc file based on staging and add PULL_REQUEST_NUMBER env to it + run: | + cp .env.staging .env.adhoc + sed -i '' 's/ENVIRONMENT=staging/ENVIRONMENT=adhoc/' .env.adhoc + echo "PULL_REQUEST_NUMBER=$PULL_REQUEST_NUMBER" >> .env.adhoc + + - name: Install node modules + run: | + npm install + cd .. && npm install + + - name: Setup Ruby + uses: ruby/setup-ruby@v1.190.0 + with: + bundler-cache: true + working-directory: 'Mobile-Expensify/react-native' + + - name: Install New Expensify Gems + run: bundle install + + - name: Cache Pod dependencies + uses: actions/cache@v4 + id: pods-cache + with: + path: ios/Pods + key: ${{ runner.os }}-pods-cache-${{ hashFiles('ios/Podfile.lock', 'firebase.json') }} + + - name: Compare Podfile.lock and Manifest.lock + id: compare-podfile-and-manifest + run: echo "IS_PODFILE_SAME_AS_MANIFEST=${{ hashFiles('ios/Podfile.lock') == hashFiles('ios/Pods/Manifest.lock') }}" >> "$GITHUB_OUTPUT" + + - name: Install cocoapods + uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 + if: steps.pods-cache.outputs.cache-hit != 'true' || steps.compare-podfile-and-manifest.outputs.IS_PODFILE_SAME_AS_MANIFEST != 'true' || steps.setup-node.outputs.cache-hit != 'true' + with: + timeout_minutes: 10 + max_attempts: 5 + command: cd Mobile-Expensify/iOS && bundle exec pod install + + - name: Install 1Password CLI + uses: 1password/install-cli-action@v1 + + - name: Load files from 1Password + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + run: | + op document get --output ./OldApp_AdHoc.mobileprovision OldApp_AdHoc + op document get --output ./OldApp_AdHoc_Share_Extension.mobileprovision OldApp_AdHoc_Share_Extension + op document get --output ./OldApp_AdHoc_Notification_Service.mobileprovision OldApp_AdHoc_Notification_Service + + - 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: Build AdHoc app + run: bundle exec fastlane ios build_adhoc_hybrid + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + 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: Upload AdHoc build to S3 + run: bundle exec fastlane ios upload_s3 + 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 + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ios + path: ./ios_paths.json + + postGithubComment: runs-on: ubuntu-latest name: Post a GitHub comment with app download links for testing - needs: [validateActor, getBranchRef, androidHybrid] + needs: [validateActor, getBranchRef, androidHybrid, iosHybrid] if: ${{ always() }} steps: - name: Checkout @@ -217,6 +333,17 @@ jobs: uses: actions/download-artifact@v4 if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} + - name: Read JSONs with iOS paths + id: get_ios_path + if: ${{ needs.iosHybrid.result == 'success' }} + run: | + content_ios="$(cat ./ios/ios_paths.json)" + content_ios="${content_ios//'%'/'%25'}" + content_ios="${content_ios//$'\n'/'%0A'}" + content_ios="${content_ios//$'\r'/'%0D'}" + ios_path=$(echo "$content_ios" | jq -r '.html_path') + echo "ios_path=$ios_path" >> "$GITHUB_OUTPUT" + - name: Publish links to apps for download if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }} uses: ./.github/actions/javascript/postTestBuildComment @@ -224,4 +351,6 @@ jobs: PR_NUMBER: ${{ env.PULL_REQUEST_NUMBER }} GITHUB_TOKEN: ${{ github.token }} ANDROID: ${{ needs.androidHybrid.result }} - ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }} \ No newline at end of file + IOS: ${{ needs.iosHybrid.result }} + ANDROID_LINK: ${{ needs.androidHybrid.outputs.S3_APK_PATH }} + IOS_LINK: ${{ steps.get_ios_path.outputs.ios_path }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000000..7b3a3d9f9432 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Mobile-Expensify"] + path = Mobile-Expensify + url = https://github.com/Expensify/Mobile-Expensify.git diff --git a/Mobile-Expensify b/Mobile-Expensify new file mode 160000 index 000000000000..c0cf9cfe6fd7 --- /dev/null +++ b/Mobile-Expensify @@ -0,0 +1 @@ +Subproject commit c0cf9cfe6fd7c760a2f8621cf0473f0694bec10f diff --git a/README.md b/README.md index 9f73a0012bef..77b9d509a74d 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,102 @@ export default withOnyx({ 1. The application uses [`react-navigation`](https://reactnavigation.org/) for navigating between parts of the app. 1. [Higher Order Components](https://reactjs.org/docs/higher-order-components.html) are used to connect React components to persistent storage via [`react-native-onyx`](https://github.com/Expensify/react-native-onyx). +---- +# HybridApp + +Currently, the production Expensify app contains both "Expensify Classic" and "New Expensify". The file structure is as follows: + +- 📂 [**App**](https://github.com/Expensify/App) + - 📂 [**android**](https://github.com/Expensify/App/tree/main/android): New Expensify Android specific code (not a part of HybridApp native code) + - 📂 [**ios**](https://github.com/Expensify/App/tree/main/ios): New Expensify iOS specific code (not a part of HybridApp native code) + - 📂 [**src**](https://github.com/Expensify/App/tree/main/src): New Expensify TypeScript logic + - 📂 [**Mobile-Expensify**](https://github.com/Expensify/Mobile-Expensify): `git` submodule that is pointed to [Mobile-Expensify](https://github.com/Expensify/Mobile-Expensify) + - 📂 [**Android**](https://github.com/Expensify/Mobile-Expensify/tree/main/Android): Expensify Classic Android specific code + - 📂 [**iOS**](https://github.com/Expensify/Mobile-Expensify/tree/main/iOS): Expensify Classic iOS specific code + - 📂 [**app**](https://github.com/Expensify/Mobile-Expensify/tree/main/app): Expensify Classic JavaScript logic (aka YAPL) + +You can only build HybridApp if you have been granted access to [`Mobile-Expensify`](https://github.com/Expensify/Mobile-Expensify). For most contributors, you will be working on the standalone NewDot application. + +## Getting started with HybridApp + +1. If you haven't, please follow [these instructions](https://github.com/Expensify/App?tab=readme-ov-file#getting-started) to setup the NewDot local environment. +2. Run `git submodule update --init` to download the `Mobile-Expensify` sourcecode. +- If you have access to `Mobile-Expensify` and the command fails with a https-related error add this to your `~/.gitconfig` file: + + ``` + [url "ssh://git@github.com/"] + insteadOf = https://github.com/ + ``` + +At this point, the default behavior of some `npm` scripts will change to target HybridApp: +- `npm run android` - build HybridApp for Android +- `npm run ios` - build HybridApp for iOS +- `npm run ipad` - build HybridApp for iPad +- `npm run ipad-sm` - build HybridApp for small iPad +- `npm run pod-install` - install pods for HybridApp +- `npm run clean` - clean native code of HybridApp + +If for some reason, you need to target the standalone NewDot application, you can append `*-standalone` to each of these scripts (eg. `npm run ios-standalone` will build NewDot instead of HybridApp). + +## Working with HybridApp +Day-to-day work with HybridApp shouldn't differ much from the work on the standalone NewDot repo. + +The main difference is that the native code which runs React Native is located in `./Mobile-Expensify/Android` and `./Mobile-Expensify/iOS` directories. It means, that changes in `./android` and `./ios` folders in the root **won't affect the HybridApp build**. + +In that case, if you'd like to eg. remove `Pods`, you need to do it in `./Mobile-Expensify/iOS`. The same rule applies to Android builds - if you'd like to delete `.cxx`, `build` or `.gradle` directories, you need to go to `./Mobile-Expensify/android` first. + +Additionally, If you'd like to open the HybridApp project in Android Studio or XCode, you **must choose a workspace located in the `Mobile-Expensify`** directory: + +- Android: `./Mobile-Expensify/Android` +- iOS: `./Mobile-Expensify/iOS/Expensify.xcworkspace` + +### Updating the Mobile-Expensify submodule + +`Mobile-Expensify` directory is a git submodule. It means, that it points to a specific commit on the `Mobile-Expensify` repository. If you'd like to download the most recent changes from `main`, please use the following command: + +`git submodule update --remote` + +### Modifying Mobile-Expensify code + +It's important to emphasise that a git submodule is just a **regular git repository** after all. It means that you can switch branches, pull the newest changes, and execute all regular git commands within the `Mobile-Expensify` directory. + +> [!Note] +> #### For external contributors +> +> If you'd like to modify the `Mobile-Expensify` source code, it is best that you create your own fork. Then, you can swap origin of the remote repository by executing this command: +> +> `cd Mobile-Expensify && git remote set-url origin ` +> +> This way, you'll attach the submodule to your fork repository. + +### Adding HybridApp-related patches + +Applying patches from the `patches` directory is performed automatically with the `npm install` command executed in `Expensify/App`. + +If you'd like to add HybridApp-specific patches, use the `--patch-dir` flag: + +`npx patch-package --patch-dir Mobile-Expensify/patches` + +### HybridApp troubleshooting + +#### Cleaning the repo +- `npm run clean` - deep clean of all HybridApp artifacts (including NewDot's `node_modules`) +- `npm run clean -- --ios` - clean only iOS HybridApp artifacts (`Pods`, `build` folder, `DerivedData`) +- `npm run clean -- --android` - clean only Android HybridApp artifacts (`.cxx`, `build`, and `.gradle` folders, execute `./gradlew clean`) + +If you'd like to do it manually, remember to `cd Mobile-Expensify` first! + +#### Common errors +1. **Please check your internet connection** - set `_isOnDev` in `api.js` to always return `false` +2. **CDN: trunk URL couldn't be downloaded** - `cd Mobile-Expensify/iOS && pod repo remove trunk` + +3. **Task :validateSigningRelease FAILED** - open `Mobile-Expensify/Android/build.gradle` and do the following: + ``` + - signingConfig signingConfigs.release + + signingConfig signingConfigs.debug + ``` +4. **Build service could not create build operation: unknown error while handling message: MsgHandlingError(message: "unable to initiate PIF transfer session (operation in progress?)")** - reopen XCode + ---- # Philosophy diff --git a/android/app/build.gradle b/android/app/build.gradle index 63d44bf8470a..23f777896f61 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 1009007400 - versionName "9.0.74-0" + versionCode 1009007500 + versionName "9.0.75-0" // 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/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index 068e4dd5bca9..68bca5228913 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -36,6 +36,12 @@ The three options for the date your report will export with are: - Submitted date: The date the employee submitted the report - Exported date: The date you export the report to NetSuite +## Accounting Method + +This dictates when reimbursable expenses will export, according to your preferred accounting method: +- Accrual: Out of pocket expenses will export immediately when the report is final approved +- Cash: Out of pocket expenses will export when paid via Expensify or marked as Reimbursed + ## Export Settings for Reimbursable Expenses **Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite. diff --git a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md index 1952ba9539cd..6f7292245f00 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Export-download-expenses.md @@ -6,7 +6,7 @@ description: Find expenses and export expense data to a CSV file Expensify allows you to export expense data to a downloaded CSV file, which you can then import into your favorite spreadsheet tool for deeper analysis. -##Search Expenses +## Search Expenses The first step to exporting and downloading expenses is finding the data you need. @@ -15,7 +15,7 @@ The first step to exporting and downloading expenses is finding the data you nee 3. Select your Filters on the top right to filter by credit card used, coding, date range, keyword, expense value and a number of other useful criteria 4. Hit View Results to see all expenses that match your filters - ##Download Expenses + ## Download Expenses 1. Select the checkbox to the left of the expenses or select all with the very top checkbox. 2. Click **# selected** at the top-right and select **Download**. diff --git a/docs/assets/images/Tax Exempt - Classic.png b/docs/assets/images/Tax Exempt - Classic.png new file mode 100644 index 000000000000..0987f5e4ca7d Binary files /dev/null and b/docs/assets/images/Tax Exempt - Classic.png differ diff --git a/docs/assets/images/Tax Exempt - New Expensify.png b/docs/assets/images/Tax Exempt - New Expensify.png new file mode 100644 index 000000000000..9ff6673da6b3 Binary files /dev/null and b/docs/assets/images/Tax Exempt - New Expensify.png differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8dbf67a150bd..880a12023056 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -71,9 +71,9 @@ platform :android do desc "Generate a production HybridApp AAB" lane :build_hybrid do - ENV["ENVFILE"]="../.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" gradle( - project_dir: '../Android', + project_dir: 'Mobile-Expensify/Android', task: "bundleRelease", flags: "--refresh-dependencies", properties: { @@ -118,7 +118,7 @@ platform :android do lane :build_local_hybrid do ENV["ENVFILE"]=".env.production" gradle( - project_dir: '../Android', + project_dir: 'Mobile-Expensify/Android', task: 'assemble', flavor: 'Production', build_type: 'Release', @@ -372,7 +372,7 @@ platform :ios do desc "Build an iOS HybridApp production build" lane :build_hybrid do - ENV["ENVFILE"]="../.env.production.hybridapp" + ENV["ENVFILE"]="Mobile-Expensify/.env.production.hybridapp" setupIOSSigningCertificate() @@ -389,7 +389,7 @@ platform :ios do ) build_app( - workspace: "../iOS/Expensify.xcworkspace", + workspace: "Mobile-Expensify/iOS/Expensify.xcworkspace", scheme: "Expensify", output_name: "Expensify.ipa", export_method: "app-store", @@ -406,6 +406,42 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp Adhoc build" + lane :build_adhoc_hybrid do + ENV["ENVFILE"]="../.env.adhoc.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AdHoc.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AdHoc_Share_Extension.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AdHoc_Notification_Service.mobileprovision" + ) + + build_app( + workspace: "../iOS/Expensify.xcworkspace", + scheme: "Expensify", + output_name: "Expensify.ipa", + export_method: "app-store", + export_options: { + manageAppVersionAndBuildNumber: false, + provisioningProfiles: { + "com.expensify.expensifylite.adhoc" => "(OldApp) AppStore", + "com.expensify.expensifylite.adhoc.SmartScanExtension" => "(OldApp) AppStore: Share Extension", + "com.expensify.expensifylite.adhoc.NotificationServiceExtension" => "(OldApp) AppStore: Notification Service", + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -418,9 +454,9 @@ platform :ios do desc "Build an unsigned iOS HybridApp production build" lane :build_unsigned_hybrid do - ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp" build_app( - workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + workspace: "./Mobile-Expensify/iOS/Expensify.xcworkspace", scheme: "Expensify" ) setIOSBuildOutputsInEnv() @@ -537,7 +573,7 @@ platform :ios do dsym_path: ENV[KEY_DSYM_PATH], gsp_path: "./ios/GoogleService-Info.plist", # Assuming we are running this from the react-native submodule directory for HybridApp - binary_path: "../iOS/Pods/FirebaseCrashlytics/upload-symbols" + binary_path: "./Mobile-Expensify/iOS/Pods/FirebaseCrashlytics/upload-symbols" ) end diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 381b10533eef..2c05d4b504bc 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.74 + 9.0.75 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.74.0 + 9.0.75.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index e2d88d194119..bdba334ae098 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.74 + 9.0.75 CFBundleSignature ???? CFBundleVersion - 9.0.74.0 + 9.0.75.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2e937386757c..70d8151a90cc 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.74 + 9.0.75 CFBundleVersion - 9.0.74.0 + 9.0.75.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile b/ios/Podfile index 4d139711ef01..41dc5179752d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -3,6 +3,7 @@ require File.join(File.dirname(`node --print "require.resolve('expo/package.json # This value is used by $RNMapboxMaps $RNMapboxMapsImpl = 'mapbox' $VCDisableFrameProcessors = true +ENV['PROJECT_ROOT_PATH'] = "./"; def node_require(script) # Resolve script with node to allow for hoisting @@ -82,6 +83,7 @@ target 'NewExpensify' do # ENV Variable enables/disables TurboModules ENV['RCT_NEW_ARCH_ENABLED'] = '1'; + use_react_native!( :path => config[:reactNativePath], # An absolute path to your application root. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c8e92768eb9a..18eba3d79c27 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3290,6 +3290,6 @@ SPEC CHECKSUMS: VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: 15e2f095b9c80d658459723edf84005a6867debf +PODFILE CHECKSUM: 615266329434ea4a994dccf622008a2197313c88 COCOAPODS: 1.15.2 diff --git a/package-lock.json b/package-lock.json index ab29133c143e..83248b124845 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.74-0", + "version": "9.0.75-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.74-0", + "version": "9.0.75-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 3c6ca5e63a43..770ad1a8c1f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.74-0", + "version": "9.0.75-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -11,12 +11,18 @@ "setupNewDotWebForEmulators": "./scripts/setup-newdot-web-emulators.sh", "startAndroidEmulator": "./scripts/start-android.sh", "postinstall": "./scripts/postInstall.sh", - "clean": "npx react-native clean-project-auto", - "android": "./scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only", - "ios": "./scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"", + "clean": "./scripts/clean.sh", + "clean-standalone": "./scripts/clean.sh --new-dot", + "android": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android", + "android-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --android --new-dot", + "ios": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios", + "ios-standalone": "./scripts/set-pusher-suffix.sh && ./scripts/run-build.sh --ios --new-dot", "pod-install": "./scripts/pod-install.sh", - "ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", - "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", + "pod-install-standalone": "./scripts/pod-install.sh --new-dot", + "ipad": "concurrently \"./scripts/run-build.sh --ipad\"", + "ipad-standalone": "concurrently \"./scripts/run-build.sh --ipad --new-dot\"", + "ipad-sm": "concurrently \"./scripts/run-build.sh --ipad-sm\"", + "ipad-sm-standalone": "concurrently \"./scripts/run-build.sh --ipad-sm --new-dot\"", "start": "npx react-native start", "web": "./scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", "web-proxy": "ts-node web/proxy.ts", diff --git a/patches/@onfido+react-native-sdk+10.6.0.patch b/patches/@onfido+react-native-sdk+10.6.0.patch index 201e9ab92c22..87f0aad1618d 100644 --- a/patches/@onfido+react-native-sdk+10.6.0.patch +++ b/patches/@onfido+react-native-sdk+10.6.0.patch @@ -1252,7 +1252,7 @@ index a9de0d0..da83d9f 100644 - s.dependency "Onfido", "~> 29.6.0" + s.dependency "Onfido", "~> 29.7.0" + -+ if ENV['USE_FRAMEWORKS'] == '1' ++ if ENV['USE_FRAMEWORKS'] != nil + s.pod_target_xcconfig = { + "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", + "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", diff --git a/patches/@react-native+gradle-plugin+0.75.2.patch b/patches/@react-native+gradle-plugin+0.75.2+001+initial.patch similarity index 100% rename from patches/@react-native+gradle-plugin+0.75.2.patch rename to patches/@react-native+gradle-plugin+0.75.2+001+initial.patch diff --git a/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch b/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch deleted file mode 100644 index 9d848520a943..000000000000 --- a/patches/@react-native-camera-roll+camera-roll+7.4.0+001+hybrid-app.patch +++ /dev/null @@ -1,15 +0,0 @@ -diff --git a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -index 6891fa3..8397f95 100644 ---- a/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -+++ b/node_modules/@react-native-camera-roll/camera-roll/android/build.gradle -@@ -81,7 +81,9 @@ def findNodeModulePath(baseDir, packageName) { - } - - def resolveReactNativeDirectory() { -- def reactNative = file("${findNodeModulePath(rootProject.projectDir, "react-native")}") -+ def projectDir = this.hasProperty('reactNativeProject') ? this.reactNativeProject : rootProject.projectDir -+ def modulePath = file(projectDir); -+ def reactNative = file("${findNodeModulePath(modulePath, 'react-native')}") - if (reactNative.exists()) { - return reactNative - } diff --git a/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch b/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch deleted file mode 100644 index 7f64391efe4c..000000000000 --- a/patches/@react-native-community+cli-platform-android+14.0.0+001+hybrid-app.patch +++ /dev/null @@ -1,52 +0,0 @@ -diff --git a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -index 43296c6..0d91033 100644 ---- a/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -+++ b/node_modules/@react-native-community/cli-platform-android/native_modules.gradle -@@ -149,16 +149,18 @@ class ReactNativeModules { - private ProviderFactory providers - private String packageName - private File root -+ private File rnRoot - private ArrayList> reactNativeModules - private HashMap reactNativeModulesBuildVariants - private String reactNativeVersion - - private static String LOG_PREFIX = ":ReactNative:" - -- ReactNativeModules(Logger logger, ProviderFactory providers, File root) { -+ ReactNativeModules(Logger logger, ProviderFactory providers, File root, File rnRoot) { - this.logger = logger - this.providers = providers - this.root = root -+ this.rnRoot = rnRoot - - def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig() - this.reactNativeModules = nativeModules -@@ -440,10 +442,10 @@ class ReactNativeModules { - */ - def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" - String[] nodeCommand = ["node", "-e", cliResolveScript] -- def cliPath = this.getCommandOutput(nodeCommand, this.root) -+ def cliPath = this.getCommandOutput(nodeCommand, this.rnRoot) - - String[] reactNativeConfigCommand = ["node", cliPath, "config", "--platform", "android"] -- def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root) -+ def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.rnRoot) - - def json - try { -@@ -513,7 +515,13 @@ class ReactNativeModules { - */ - def projectRoot = rootProject.projectDir - --def autoModules = new ReactNativeModules(logger, providers, projectRoot) -+def autoModules -+ -+if(this.hasProperty('reactNativeProject')){ -+ autoModules = new ReactNativeModules(logger, providers, projectRoot, new File(projectRoot, reactNativeProject)) -+} else { -+ autoModules = new ReactNativeModules(logger, providers, projectRoot, projectRoot) -+} - - def reactNativeVersionRequireNewArchEnabled(autoModules) { - def rnVersion = autoModules.reactNativeVersion diff --git a/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch b/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch deleted file mode 100644 index e54ab17c43dd..000000000000 --- a/patches/@react-native-community+cli-platform-ios+14.0.0+001+hybrid-app.patch +++ /dev/null @@ -1,46 +0,0 @@ -diff --git a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -index 82f537c..df441e2 100644 ---- a/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -+++ b/node_modules/@react-native-community/cli-platform-ios/native_modules.rb -@@ -12,7 +12,7 @@ - require 'pathname' - require 'cocoapods' - --def use_native_modules!(config = nil) -+def updateConfig(config = nil) - if (config.is_a? String) - Pod::UI.warn("Passing custom root to use_native_modules! is deprecated.", - [ -@@ -24,7 +24,6 @@ def use_native_modules!(config = nil) - # Resolving the path the RN CLI. The `@react-native-community/cli` module may not be there for certain package managers, so we fall back to resolving it through `react-native` package, that's always present in RN projects - cli_resolve_script = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}" - cli_bin = Pod::Executable.execute_command("node", ["-e", cli_resolve_script], true).strip -- - if (!config) - json = [] - -@@ -36,9 +35,24 @@ def use_native_modules!(config = nil) - - config = JSON.parse(json.join("\n")) - end -+end -+ -+def use_native_modules!(config = nil) -+ if (ENV['REACT_NATIVE_DIR']) -+ Dir.chdir(ENV['REACT_NATIVE_DIR']) do -+ config = updateConfig(config) -+ end -+ else -+ config = updateConfig(config) -+ end - - project_root = Pathname.new(config["project"]["ios"]["sourceDir"]) - -+ if(ENV["PROJECT_ROOT_DIR"]) -+ project_root = File.join(Dir.pwd, ENV["PROJECT_ROOT_DIR"]) -+ -+ end -+ - packages = config["dependencies"] - found_pods = [] - diff --git a/patches/expo+51.0.31+001+hybrid-app.patch b/patches/expo+51.0.31+001+hybrid-app.patch deleted file mode 100644 index 44048857fc1b..000000000000 --- a/patches/expo+51.0.31+001+hybrid-app.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/expo/scripts/autolinking.gradle b/node_modules/expo/scripts/autolinking.gradle -index 929b7f0..c948ebb 100644 ---- a/node_modules/expo/scripts/autolinking.gradle -+++ b/node_modules/expo/scripts/autolinking.gradle -@@ -1,6 +1,6 @@ - // Resolve `expo` > `expo-modules-autolinking` dependency chain - def autolinkingPath = ["node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })"] --apply from: new File( -+apply from: hasProperty("reactNativeProject") ? file('../../expo-modules-autolinking/scripts/android/autolinking_implementation.gradle') : new File( - providers.exec { - workingDir(rootDir) - commandLine(autolinkingPath) diff --git a/patches/expo-av+14.0.7+001+hybrid-app.patch b/patches/expo-av+14.0.7+001+hybrid-app.patch deleted file mode 100644 index 4cf0dee990c5..000000000000 --- a/patches/expo-av+14.0.7+001+hybrid-app.patch +++ /dev/null @@ -1,19 +0,0 @@ -diff --git a/node_modules/expo-av/android/build.gradle b/node_modules/expo-av/android/build.gradle -index 11e7574..6dae6a0 100644 ---- a/node_modules/expo-av/android/build.gradle -+++ b/node_modules/expo-av/android/build.gradle -@@ -3,12 +3,13 @@ apply plugin: 'com.android.library' - group = 'host.exp.exponent' - version = '14.0.7' - -+def REACT_NATIVE_PATH = this.hasProperty('reactNativeProject') ? this.reactNativeProject + '/node_modules/react-native/package.json' : 'react-native/package.json' - def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":ReactAndroid") != null - def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE - ? findProject(":ReactAndroid").getProjectDir().parent - : file(providers.exec { - workingDir(rootDir) -- commandLine("node", "--print", "require.resolve('react-native/package.json')") -+ commandLine("node", "--print", "require.resolve('${REACT_NATIVE_PATH}')") - }.standardOutput.asText.get().trim()).parent - - def reactNativeArchitectures() { diff --git a/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch b/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch deleted file mode 100644 index a345f84b8f20..000000000000 --- a/patches/expo-modules-autolinking+1.11.2+001+hybrid-app.patch +++ /dev/null @@ -1,40 +0,0 @@ -diff --git a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -index f085818..fcb9594 100644 ---- a/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -+++ b/node_modules/expo-modules-autolinking/scripts/android/autolinking_implementation.gradle -@@ -152,12 +152,13 @@ class ExpoAutolinkingManager { - } - - static private String[] convertOptionsToCommandArgs(String command, Map options) { -+ def expoPath = options.searchPaths ? "../react-native/node_modules/expo" : "expo" - String[] args = [ - 'node', - '--no-warnings', - '--eval', - // Resolve the `expo` > `expo-modules-autolinking` chain from the project root -- 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo\')] }))(process.argv.slice(1))', -+ "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'${expoPath}\')] }))(process.argv.slice(1))", - '--', - command, - '--platform', -diff --git a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -index 5d46f1e..fec4f34 100644 ---- a/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -+++ b/node_modules/expo-modules-autolinking/scripts/ios/project_integrator.rb -@@ -215,6 +215,7 @@ module Expo - args = autolinking_manager.base_command_args.map { |arg| "\"#{arg}\"" } - platform = autolinking_manager.platform_name.downcase - package_names = autolinking_manager.packages_to_generate.map { |package| "\"#{package.name}\"" } -+ expo_path = ENV['REACT_NATIVE_DIR'] ? "#{ENV['REACT_NATIVE_DIR']}/node_modules/expo" : "expo" - - <<~SUPPORT_SCRIPT - #!/usr/bin/env bash -@@ -262,7 +263,7 @@ module Expo - - with_node \\ - --no-warnings \\ -- --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))" \\ -+ --eval "require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'#{expo_path}/package.json\')] }))(process.argv.slice(1))" \\ - generate-modules-provider #{args.join(' ')} \\ - --target "#{modules_provider_path}" \\ - --platform "apple" \\ diff --git a/patches/expo-modules-core+1.12.23+002+hybrid-app.patch b/patches/expo-modules-core+1.12.23+002+hybrid-app.patch deleted file mode 100644 index b32830615aaa..000000000000 --- a/patches/expo-modules-core+1.12.23+002+hybrid-app.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/node_modules/expo-modules-core/android/build.gradle b/node_modules/expo-modules-core/android/build.gradle -index f22a3c3..4884cea 100644 ---- a/node_modules/expo-modules-core/android/build.gradle -+++ b/node_modules/expo-modules-core/android/build.gradle -@@ -20,12 +20,13 @@ def isExpoModulesCoreTests = { - }.call() - - def REACT_NATIVE_BUILD_FROM_SOURCE = findProject(":packages:react-native:ReactAndroid") != null --def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE -- ? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent -- : file(providers.exec { -+def FALLBACK_REACT_NATIVE_DIR = hasProperty("reactNativeProject") ? file('../../react-native') : file(providers.exec { - workingDir(rootDir) - commandLine("node", "--print", "require.resolve('react-native/package.json')") - }.standardOutput.asText.get().trim()).parent -+def REACT_NATIVE_DIR = REACT_NATIVE_BUILD_FROM_SOURCE -+ ? findProject(":packages:react-native:ReactAndroid").getProjectDir().parent -+ : FALLBACK_REACT_NATIVE_DIR - - def reactProperties = new Properties() - file("$REACT_NATIVE_DIR/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } diff --git a/patches/react-native-plaid-link-sdk+11.11.0.patch b/patches/react-native-plaid-link-sdk+11.11.0.patch index 28e492f6999f..39ae7b3cd1e7 100644 --- a/patches/react-native-plaid-link-sdk+11.11.0.patch +++ b/patches/react-native-plaid-link-sdk+11.11.0.patch @@ -23,7 +23,7 @@ index 7c60081..4a13a3c 100644 # we don't want this to be seen by Swift s.private_header_files = 'ios/PLKFabricHelpers.h' -+ if ENV['USE_FRAMEWORKS'] == '1' ++ if ENV['USE_FRAMEWORKS'] != nil + s.pod_target_xcconfig = { + "OTHER_CFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", + "OTHER_CPLUSPLUSFLAGS" => "$(inherited) -DUSE_FRAMEWORKS", diff --git a/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch b/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch deleted file mode 100644 index 835df1f034a9..000000000000 --- a/patches/react-native-reanimated+3.16.3+001+hybrid-app.patch +++ /dev/null @@ -1,17 +0,0 @@ -diff --git a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -index 9fc7b15..e453d84 100644 ---- a/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -+++ b/node_modules/react-native-reanimated/scripts/reanimated_utils.rb -@@ -17,7 +17,11 @@ def find_config() - :react_native_reanimated_dir_from_pods_root => nil, - } - -- react_native_node_modules_dir = File.join(File.dirname(`cd "#{Pod::Config.instance.installation_root.to_s}" && node --print "require.resolve('react-native/package.json')"`), '..') -+ root_project = Pod::Config.instance.installation_root.to_s -+ if(ENV['PROJECT_ROOT_DIR']) -+ root_project = ENV['PROJECT_ROOT_DIR'] -+ end -+ react_native_node_modules_dir = File.join(File.dirname(`cd "#{root_project}" && node --print "require.resolve('react-native/package.json')"`), '..') - react_native_json = try_to_parse_react_native_package_json(react_native_node_modules_dir) - - if react_native_json == nil diff --git a/react-native.config.js b/react-native.config.js index 6d6dd3f5805f..773375378acd 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,7 +1,10 @@ +const iosSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'ios' : 'ios'; +const androidSourceDir = process.env.PROJECT_ROOT_PATH ? process.env.PROJECT_ROOT_PATH + 'android' : 'android'; + module.exports = { project: { - ios: {sourceDir: 'ios'}, - android: {}, + ios: {sourceDir: iosSourceDir}, + android: {sourceDir: androidSourceDir}, }, assets: ['./assets/fonts/native'], }; diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh index 4ce023755258..29e121acc968 100755 --- a/scripts/applyPatches.sh +++ b/scripts/applyPatches.sh @@ -9,9 +9,15 @@ source "$SCRIPTS_DIR/shellUtils.sh" # Wrapper to run patch-package. function patchPackage { + # See if we're in the HybridApp repo + IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + OS="$(uname)" if [[ "$OS" == "Darwin" || "$OS" == "Linux" ]]; then npx patch-package --error-on-fail --color=always + if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then + npx patch-package --patch-dir 'Mobile-Expensify/patches' --error-on-fail --color=always + fi else error "Unsupported OS: $OS" exit 1 diff --git a/scripts/clean.sh b/scripts/clean.sh new file mode 100755 index 000000000000..1ecd73731b61 --- /dev/null +++ b/scripts/clean.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +BLUE='\033[1;34m' +NC='\033[0m' + +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$1" != "--new-dot" ]]; then + echo -e "${BLUE}Cleaning HybridApp project...${NC}" + # Navigate to Mobile-Expensify repository, and clean + cd Mobile-Expensify + npm run clean -- "$@" +else + # Clean NewDot + echo -e "${BLUE}Cleaning standalone NewDot project...${NC}" + npx react-native clean-project-auto +fi diff --git a/scripts/is-hybrid-app.sh b/scripts/is-hybrid-app.sh new file mode 100755 index 000000000000..32ca190ac832 --- /dev/null +++ b/scripts/is-hybrid-app.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +if [[ ! -d Mobile-Expensify ]]; then + echo false + exit 0 +else + cd Mobile-Expensify +fi + +# Check if 'package.json' exists +if [[ -f package.json ]]; then + # Read the 'name' field from 'package.json' + package_name=$(jq -r '.name' package.json 2>/dev/null) + + # Check if the 'name' field is 'mobile-expensify' + if [[ "$package_name" == "mobile-expensify" ]]; then + echo true + exit 0 + fi +else + echo "package.json not found in Mobile-Expensify" + echo false +fi diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index cb2976d64587..8e38f1706d6f 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -8,6 +8,9 @@ # Exit immediately if any command exits with a non-zero status set -e +BLUE='\033[1;34m' +NC='\033[0m' + # Go to project root START_DIR="$(pwd)" ROOT_DIR="$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")" @@ -40,6 +43,25 @@ if ! yq --version > /dev/null 2>&1; then cleanupAndExit 1 fi +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) +NEW_DOT_FLAG="false" + +if [ "$1" == "--new-dot" ]; then + NEW_DOT_FLAG="true" +fi + +if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + echo -e "${BLUE}Executing npm run pod-install for HybridApp...${NC}" + # Navigate to the OldDot repository, and run bundle install and pod install + cd Mobile-Expensify/ios + bundle install + bundle exec pod install + exit 0 +fi + +echo -e "${BLUE}Executing npm run pod-install for standalone NewDot...${NC}" + CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs' if [ -d "$CACHED_PODSPEC_DIR" ]; then info "Verifying pods from Podfile.lock match local podspecs..." diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index 782c8ef5822c..db24f04f8a6c 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -7,6 +7,16 @@ set -e ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") cd "$ROOT_DIR" || exit 1 +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + +if [[ "$IS_HYBRID_APP_REPO" == "true" ]]; then + cd Mobile-Expensify || exit 1 + npm i + + cd "$ROOT_DIR" || exit 1 +fi + # Apply packages using patch-package scripts/applyPatches.sh diff --git a/scripts/run-build.sh b/scripts/run-build.sh new file mode 100755 index 000000000000..7689aabbbf59 --- /dev/null +++ b/scripts/run-build.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -e + +export PROJECT_ROOT_PATH + +BUILD="$1" +NEW_DOT_FLAG="false" +IOS_MODE="DebugDevelopment" +ANDROID_MODE="developmentDebug" +SCHEME="New Expensify Dev" +APP_ID="com.expensify.chat.dev" + +GREEN='\033[1;32m' +RED='\033[1;31m' +NC='\033[0m' + +# Function to print error message and exit +function print_error_and_exit { + echo -e "${RED}Error: Invalid invocation. Please use one of: [ios, ipad, ipad-sm, android].${NC}" + exit 1 +} + +# Assign the arguments to variables +if [ "$#" -eq 1 ]; then + BUILD="$1" +elif [ "$#" -eq 2 ]; then + if [ "$1" == "--new-dot" ]; then + BUILD="$2" + NEW_DOT_FLAG="true" + elif [ "$2" == "--new-dot" ]; then + BUILD="$1" + NEW_DOT_FLAG="true" + else + print_error_and_exit + fi +else + print_error_and_exit +fi + +# See if we're in the HybridApp repo +IS_HYBRID_APP_REPO=$(scripts/is-hybrid-app.sh) + + if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then + # Set HybridApp-specific arguments + IOS_MODE="Debug" + ANDROID_MODE="Debug" + SCHEME="Expensify" + APP_ID="org.me.mobiexpensifyg" + + echo -e "\n${GREEN}Starting a HybridApp build!${NC}" + PROJECT_ROOT_PATH="Mobile-Expensify/" + export CUSTOM_APK_NAME="Expensify-debug.apk" +else + echo -e "\n${GREEN}Starting a standalone NewDot build!${NC}" + echo $ANDROID_MODE + PROJECT_ROOT_PATH="./" + unset CUSTOM_APK_NAME +fi + +# Check if the argument is one of the desired values +case "$BUILD" in + --ios) + npx react-native run-ios --list-devices --mode $IOS_MODE --scheme "$SCHEME" + ;; + --ipad) + npx react-native run-ios --simulator "iPad Pro (12.9-inch) (6th generation)" --mode $IOS_MODE --scheme "$SCHEME" + ;; + --ipad-sm) + npx react-native run-ios --simulator "iPad Pro (11-inch) (4th generation)" --mode $IOS_MODE --scheme "$SCHEME" + ;; + --android) + npx react-native run-android --mode $ANDROID_MODE --appId $APP_ID --active-arch-only + ;; + *) + print_error_and_exit + ;; +esac diff --git a/src/CONST.ts b/src/CONST.ts index 2d38d26d8820..15554719ca9d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -349,6 +349,7 @@ const CONST = { SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, MAX_IMAGE_CANVAS_AREA: 16777216, + CHUNK_LOAD_ERROR: 'ChunkLoadError', API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 6be2b43c09d7..12189d22dba0 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -39,7 +39,7 @@ type AmountTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; -} & Pick; +} & Pick; function AmountTextInput( { diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts index 0d8acd5eef38..336d7043d4ed 100644 --- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts +++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts @@ -2,6 +2,7 @@ import type {FlashList} from '@shopify/flash-list'; import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import emojis from '@assets/emojis'; import {useFrequentlyUsedEmojis} from '@components/OnyxProvider'; +import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePreferredEmojiSkinTone from '@hooks/usePreferredEmojiSkinTone'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -23,12 +24,15 @@ const useEmojiPickerMenu = () => { const [preferredSkinTone] = usePreferredEmojiSkinTone(); const {windowHeight} = useWindowDimensions(); const StyleUtils = useStyleUtils(); + const {keyboardHeight} = useKeyboardState(); + /** - * At EmojiPicker has set innerContainerStyle with maxHeight: '95%' by styles.popoverInnerContainer - * to avoid the list style to be cut off due to the list height being larger than the container height - * so we need to calculate listStyle based on the height of the window and innerContainerStyle at the EmojiPicker + * The EmojiPicker sets the `innerContainerStyle` with `maxHeight: '95%'` in `styles.popoverInnerContainer` + * to prevent the list from being cut off when the list height exceeds the container's height. + * To calculate the available list height, we subtract the keyboard height from the `windowHeight` + * to ensure the list is properly adjusted when the keyboard is visible. */ - const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95); + const listStyle = StyleUtils.getEmojiPickerListHeight(isListFiltered, windowHeight * 0.95 - keyboardHeight); useEffect(() => { setFilteredEmojis(allEmojis); diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx index f56441316f7c..880c833e18e8 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx +++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx @@ -27,7 +27,7 @@ function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseEr return ( : } + FallbackComponent={updateRequired ? UpdateRequiredView : GenericErrorPage} onError={catchError} > {children} diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 9ef33900bb00..717659c16fd3 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; +import type {TextInputWithCurrencySymbolProps} from './TextInputWithCurrencySymbol/types'; type CurrentMoney = {amount: string; currency: string}; @@ -91,7 +92,7 @@ type MoneyRequestAmountInputProps = { /** The width of inner content */ contentWidth?: number; -}; +} & Pick; type Selection = { start: number; @@ -126,6 +127,7 @@ function MoneyRequestAmountInput( hideFocusedState = true, shouldKeepUserInput = false, autoGrow = true, + autoGrowExtraSpace, contentWidth, ...props }: MoneyRequestAmountInputProps, @@ -289,6 +291,7 @@ function MoneyRequestAmountInput( return ( { + if (!errors.includes(formError)) { + return; + } + + setFormError(''); + }, + [formError, setFormError], + ); + const shouldDisplayFieldError: boolean = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -305,6 +316,32 @@ function MoneyRequestConfirmationList({ const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0); + useEffect(() => { + // We want this effect to run only when the transaction is moving from Self DM to a workspace chat + if (!isDistanceRequest || !isMovingTransactionFromTrackExpense || !isPolicyExpenseChat) { + return; + } + + const errorKey = 'iou.error.invalidRate'; + const policyRates = DistanceRequestUtils.getMileageRates(policy); + + // If the selected rate belongs to the policy, clear the error + if (Object.keys(policyRates).includes(customUnitRateID)) { + clearFormErrors([errorKey]); + return; + } + + // If there is a distance rate in the policy that matches the rate and unit of the currently selected mileage rate, select it automatically + const matchingRate = Object.values(policyRates).find((policyRate) => policyRate.rate === mileageRate.rate && policyRate.unit === mileageRate.unit); + if (matchingRate?.customUnitRateID) { + IOU.setCustomUnitRateID(transactionID, matchingRate.customUnitRateID); + return; + } + + // If none of the above conditions are met, display the rate error + setFormError(errorKey); + }, [isDistanceRequest, isPolicyExpenseChat, transactionID, mileageRate, customUnitRateID, policy, isMovingTransactionFromTrackExpense, setFormError, clearFormErrors]); + useEffect(() => { if (shouldDisplayFieldError && didConfirmSplit) { setFormError('iou.error.genericSmartscanFailureMessage'); @@ -315,7 +352,7 @@ function MoneyRequestConfirmationList({ return; } // reset the form error whenever the screen gains or loses focus - setFormError(''); + clearFormErrors(['iou.error.genericSmartscanFailureMessage', 'iou.receiptScanningFailed']); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); @@ -470,8 +507,8 @@ function MoneyRequestConfirmationList({ return; } - setFormError(''); - }, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate]); + clearFormErrors(['iou.error.invalidSplit', 'iou.error.invalidSplitParticipants', 'iou.error.invalidSplitYourself']); + }, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate, clearFormErrors]); useEffect(() => { if (!isTypeSplit || !transaction?.splitShares) { @@ -638,7 +675,9 @@ function MoneyRequestConfirmationList({ }, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]); useEffect(() => { - if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { + if (!isDistanceRequest || (isMovingTransactionFromTrackExpense && !isPolicyExpenseChat)) { + // We don't want to recalculate the distance merchant when moving a transaction from Track Expense to a 1:1 chat, because the distance rate will be the same default P2P rate. + // When moving to a policy chat (e.g. sharing with an accountant), we should recalculate the distance merchant with the policy's rate. return; } @@ -661,6 +700,7 @@ function MoneyRequestConfirmationList({ translate, toLocaleDigit, isDistanceRequest, + isPolicyExpenseChat, transaction, transactionID, action, diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 01756e11aadf..c391564a6d3d 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -259,6 +259,7 @@ function MoneyRequestConfirmationListFooter({ const taxRateTitle = TransactionUtils.getTaxName(policy, transaction); // Determine if the merchant error should be displayed const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty; + const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate'; // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") const shouldShowReceiptEmptyState = iouType === CONST.IOU.TYPE.SUBMIT && PolicyUtils.isPaidGroupPolicy(policy); const { @@ -369,6 +370,7 @@ function MoneyRequestConfirmationListFooter({ style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + brickRoadIndicator={shouldDisplayDistanceRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} disabled={didConfirm} interactive={!!rate && !isReadOnly && isPolicyExpenseChat} /> @@ -429,7 +431,12 @@ function MoneyRequestConfirmationListFooter({ title={iouCategory} description={translate('common.category')} numberOfLinesTitle={2} - onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID))} + onPress={() => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID), + CONST.NAVIGATION.ACTION_TYPE.PUSH, + ) + } style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} disabled={didConfirm} diff --git a/src/components/RNMarkdownTextInput.tsx b/src/components/RNMarkdownTextInput.tsx index d36af6e13826..11f8a852dbcf 100644 --- a/src/components/RNMarkdownTextInput.tsx +++ b/src/components/RNMarkdownTextInput.tsx @@ -5,13 +5,14 @@ import React from 'react'; import type {TextInput} from 'react-native'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; +import CONST from '@src/CONST'; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextInput); type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & TextInput & HTMLInputElement; -function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: ForwardedRef) { +function RNMarkdownTextInputWithRef({maxLength, ...props}: MarkdownTextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -27,6 +28,10 @@ function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: Forwarde }} // eslint-disable-next-line {...props} + /** + * If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text + */ + maxLength={maxLength ?? CONST.MAX_COMMENT_LENGTH + 1} /> ); } diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index de20575aeef4..d1dcdb2f57f5 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -79,8 +79,10 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !ReportUtils.isReportFieldDisabled(report, reportField, policy)); const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && ReportUtils.isReportFieldOfTypeTitle(enabledReportFields.at(0)); - const shouldShowReportField = - !ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ReportUtils.isPaidGroupPolicyExpenseReport(report) && (!isCombinedReport || !isOnlyTitleFieldEnabled); + const isClosedExpenseReportWithNoExpenses = ReportUtils.isClosedExpenseReportWithNoExpenses(report); + const isPaidGroupPolicyExpenseReport = ReportUtils.isPaidGroupPolicyExpenseReport(report); + const isInvoiceReport = ReportUtils.isInvoiceReport(report); + const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && (!isCombinedReport || !isOnlyTitleFieldEnabled); const renderThreadDivider = useMemo( () => @@ -102,9 +104,9 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo <> - {!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && ( + {!isClosedExpenseReportWithNoExpenses && ( <> - {ReportUtils.isPaidGroupPolicyExpenseReport(report) && + {(isPaidGroupPolicyExpenseReport || isInvoiceReport) && policy?.areReportFieldsEnabled && (!isCombinedReport || !isOnlyTitleFieldEnabled) && sortedPolicyReportFields.map((reportField) => { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index ca50e93e536f..1c2a552db476 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -468,8 +468,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals return; } if (parentReportAction) { - const urlToNavigateBack = IOU.cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true); - Navigation.goBack(urlToNavigateBack); + IOU.cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true); return; } } diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index db5077beca9a..c7e7f587769c 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import Badge from '@components/Badge'; import Button from '@components/Button'; @@ -36,7 +36,7 @@ type ActionCellProps = { function ActionCell({ action = CONST.SEARCH.ACTION_TYPES.VIEW, - shouldUseSuccessStyle = true, + shouldUseSuccessStyle: shouldUseSuccessStyleProp = true, isLargeScreenWidth = true, isSelected = false, goToItem, @@ -52,6 +52,16 @@ function ActionCell({ const text = translate(actionTranslationsMap[action]); + const getButtonInnerStyles = useCallback( + (shouldUseSuccessStyle: boolean) => { + if (!isSelected) { + return {}; + } + return shouldUseSuccessStyle ? styles.buttonSuccessHovered : styles.buttonDefaultHovered; + }, + [isSelected, styles], + ); + const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID); if ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE) { @@ -78,30 +88,28 @@ function ActionCell({ } if (action === CONST.SEARCH.ACTION_TYPES.VIEW || shouldUseViewAction) { - const buttonInnerStyles = isSelected ? styles.buttonDefaultHovered : {}; return isLargeScreenWidth ? (