diff --git a/.eslintrc.js b/.eslintrc.js index 5f450f3ae6c2..cfbfdcc8fe91 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -109,7 +109,6 @@ module.exports = { 'plugin:prettier/recommended', ], plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash', 'deprecation'], - ignorePatterns: ['lib/**'], parser: '@typescript-eslint/parser', parserOptions: { project: path.resolve(__dirname, './tsconfig.json'), diff --git a/.github/actions/composite/setupGitForOSBotify/action.yml b/.github/actions/composite/setupGitForOSBotify/action.yml index c61fa7e934fd..456cef93676a 100644 --- a/.github/actions/composite/setupGitForOSBotify/action.yml +++ b/.github/actions/composite/setupGitForOSBotify/action.yml @@ -20,10 +20,10 @@ runs: - name: Set up git for OSBotify shell: bash run: | - git config user.signingkey AEE1036472A782AB - git config commit.gpgsign true - git config user.name OSBotify - git config user.email infra+osbotify@expensify.com + git config --global user.signingkey AEE1036472A782AB + git config --global commit.gpgsign true + git config --global user.name OSBotify + git config --global user.email infra+osbotify@expensify.com - name: Enable debug logs for git shell: bash diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml index 40dfc05e5448..aadf433fba2f 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/action.yml +++ b/.github/actions/javascript/markPullRequestsAsDeployed/action.yml @@ -17,12 +17,18 @@ inputs: ANDROID: description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + ANDROID_HYBRID: + description: "Android job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true DESKTOP: description: "Desktop job result ('success', 'failure', 'cancelled', or 'skipped')" required: true IOS: description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" required: true + IOS_HYBRID: + description: "iOS job result ('success', 'failure', 'cancelled', or 'skipped')" + required: true WEB: description: "Web job result ('success', 'failure', 'cancelled', or 'skipped')" required: true diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 62d326c9af3a..c67f38ca1f3e 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -12710,8 +12710,10 @@ async function run() { const isProd = ActionUtils.getJSONInput('IS_PRODUCTION_DEPLOY', { required: true }); const version = core.getInput('DEPLOY_VERSION', { required: true }); const androidResult = getDeployTableMessage(core.getInput('ANDROID', { required: true })); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', { required: true })); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', { required: true })); const iOSResult = getDeployTableMessage(core.getInput('IOS', { required: true })); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', { required: true })); const webResult = getDeployTableMessage(core.getInput('WEB', { required: true })); const date = core.getInput('DATE'); const note = core.getInput('NOTE'); @@ -12724,6 +12726,7 @@ async function run() { message += `πŸš€`; message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; + message += `\nπŸ€–πŸ”„ android HybridApp πŸ€–πŸ”„|${androidHybridResult}\nπŸŽπŸ”„ iOS HybridApp πŸŽπŸ”„|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len message += diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts index 9c2defebd01d..ba61c31a6bb2 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts +++ b/.github/actions/javascript/markPullRequestsAsDeployed/markPullRequestsAsDeployed.ts @@ -51,8 +51,10 @@ async function run() { const version = core.getInput('DEPLOY_VERSION', {required: true}); const androidResult = getDeployTableMessage(core.getInput('ANDROID', {required: true}) as PlatformResult); + const androidHybridResult = getDeployTableMessage(core.getInput('ANDROID_HYBRID', {required: true}) as PlatformResult); const desktopResult = getDeployTableMessage(core.getInput('DESKTOP', {required: true}) as PlatformResult); const iOSResult = getDeployTableMessage(core.getInput('IOS', {required: true}) as PlatformResult); + const iOSHybridResult = getDeployTableMessage(core.getInput('IOS_HYBRID', {required: true}) as PlatformResult); const webResult = getDeployTableMessage(core.getInput('WEB', {required: true}) as PlatformResult); const date = core.getInput('DATE'); @@ -67,6 +69,7 @@ async function run() { message += `πŸš€`; message += `\n\nplatform | result\n---|---\nπŸ€– android πŸ€–|${androidResult}\nπŸ–₯ desktop πŸ–₯|${desktopResult}`; message += `\n🍎 iOS 🍎|${iOSResult}\nπŸ•Έ web πŸ•Έ|${webResult}`; + message += `\nπŸ€–πŸ”„ android HybridApp πŸ€–πŸ”„|${androidHybridResult}\nπŸŽπŸ”„ iOS HybridApp πŸŽπŸ”„|${iOSHybridResult}`; if (deployVerb === 'Cherry-picked' && !/no ?qa/gi.test(prTitle ?? '')) { // eslint-disable-next-line max-len diff --git a/.github/workflows/buildAndroid.yml b/.github/workflows/buildAndroid.yml index c88542068b92..90ed0e46661a 100644 --- a/.github/workflows/buildAndroid.yml +++ b/.github/workflows/buildAndroid.yml @@ -171,7 +171,7 @@ jobs: continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-prefix }}android-artifact-aab + name: ${{ inputs.artifact-prefix }}android-aab-artifact path: ${{ steps.build.outputs.AAB_PATH }} - name: Upload Android APK artifact @@ -187,5 +187,5 @@ jobs: continue-on-error: true uses: actions/upload-artifact@v4 with: - name: ${{ inputs.artifact-prefix }}android-artifact-sourcemaps + name: ${{ inputs.artifact-prefix }}android-sourcemaps-artifact path: ./android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 34a5c356356e..dc4de9ec31a7 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -4,7 +4,7 @@ on: issue_comment: types: [created] pull_request_target: - types: [opened, closed, synchronize] + types: [opened, synchronize] jobs: CLA: diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index ca9a128e848d..93fe07be9298 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -101,3 +101,92 @@ jobs: uses: ./.github/actions/composite/announceFailedWorkflowInSlack with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + + createNewHybridVersion: + 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 + with: + poll-interval-seconds: 10 + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Check out `App` repo + uses: actions/checkout@v4 + with: + ref: main + # 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 + run: | + cd react-native + git submodule update --init + + - name: Setup git for OSBotify + uses: ./.github/actions/composite/setupGitForOSBotify + id: setupGitForOSBotify + with: + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Generate HybridApp version + run: | + # 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}') + FULL_APP_VERSION="$SHORT_APP_VERSION.$BUILD_NUMBER" + ANDROID_VERSION_CODE=$(echo "$FULL_APP_VERSION" | ruby -e "puts '05%02d%02d%02d%02d' % STDIN.read.split('.')") + + # File paths to update + ANDROID_MANIFEST_FILE="Android/AndroidManifest.xml" + IOS_INFO_PLIST_FILE="iOS/Expensify/Expensify-Info.plist" + IOS_SHARE_EXTENSION_PLIST_FILE="iOS/SmartScanExtension/Info.plist" + JS_CONFIG_FILE="app/config/config.json" + + # Update Android HybridApp Version + sed -i .bak -E "s/versionName=\"([0-9\.]*)\"/versionName=\"$FULL_APP_VERSION\"/" $ANDROID_MANIFEST_FILE + sed -i .bak -E "s/versionCode=\"([0-9]*)\"/versionCode=\"$ANDROID_VERSION_CODE\"/" $ANDROID_MANIFEST_FILE + + # Update iOS HybridApp Version + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_INFO_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_INFO_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $SHORT_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $FULL_APP_VERSION" $IOS_SHARE_EXTENSION_PLIST_FILE + + # Update JS HybridApp Version + sed -i .bak -E "s/\"version\": \"([0-9\.]*)\"/\"version\": \"$FULL_APP_VERSION\"/" $JS_CONFIG_FILE + env: + NEW_VERSION: ${{ needs.createNewVersion.outputs.NEW_VERSION }} + + - name: Commit new version + run: | + git add \ + ./Android/AndroidManifest.xml \ + ./app/config/config.json \ + ./iOS/Expensify/Expensify-Info.plist\ + ./iOS/SmartScanExtension/Info.plist + git commit -m "Update version to ${{ needs.createNewVersion.outputs.NEW_VERSION }}" + + - name: Update main branch + run: git push origin main + + - name: Announce failed workflow in Slack + if: ${{ failure() }} + uses: ./.github/actions/composite/announceFailedWorkflowInSlack + with: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4ff1a2004d8f..65d0da453068 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -96,7 +96,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/artifacts - pattern: android-artifact-* + pattern: android-*-artifact merge-multiple: true - name: Log downloaded artifact paths @@ -163,6 +163,133 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + android_hybrid: + name: Build and deploy Android HybridApp + needs: prep + runs-on: ubuntu-latest-xl + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + defaults: + run: + working-directory: Mobile-Expensify/react-native + env: + RUBYOPT: '-rostruct' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + repository: 'Expensify/Mobile-Expensify' + submodules: true + path: 'Mobile-Expensify' + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Update submodule + run: | + git submodule update --init + + - 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 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'oracle' + java-version: '17' + + - 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: 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 ./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 + + - name: Load Android upload keystore credentials from 1Password + id: load-credentials + uses: 1password/load-secrets-action@v2 + with: + export-env: false + env: + OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + ANDROID_UPLOAD_KEYSTORE_PASSWORD: op://Mobile-Deploy-CI/Repository-Secrets/ANDROID_UPLOAD_KEYSTORE_PASSWORD + 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 + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane android build_hybrid + env: + ANDROID_UPLOAD_KEYSTORE_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_PASSWORD }} + ANDROID_UPLOAD_KEYSTORE_ALIAS: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEYSTORE_ALIAS }} + ANDROID_UPLOAD_KEY_PASSWORD: ${{ steps.load-credentials.outputs.ANDROID_UPLOAD_KEY_PASSWORD }} + + - name: Upload Android app to Google Play + run: bundle exec fastlane android upload_google_play_internal_hybrid + env: + VERSION: ${{ steps.getAndroidVersion.outputs.VERSION_CODE }} + + - name: Upload Android 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=@${{ env.aabPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload Android build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: android-hybrid-build-artifact + path: ${{ env.aabPath }} + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Warn deployers if Android production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `πŸ’₯ Android HybridApp production deploy failed. Please manually submit ${{ needs.prep.outputs.APP_VERSION }} in the . πŸ’₯`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + desktop: name: Build and deploy Desktop needs: prep @@ -329,6 +456,154 @@ jobs: GITHUB_TOKEN: ${{ github.token }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + iOS_hybrid: + name: Build and deploy iOS HybridApp + needs: prep + runs-on: macos-13-xlarge + env: + DEVELOPER_DIR: /Applications/Xcode_15.2.0.app/Contents/Developer + # Only deploy HybridApp to staging + if: ${{ github.ref == 'refs/heads/staging' }} + 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 }} + + - name: Update submodule + run: | + git submodule update --init + + - 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-dependency-path: 'Mobile-Expensify/react-native' + + - 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 && 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_AppStore.mobileprovision OldApp_AppStore + op document get --output ./OldApp_AppStore_Share_Extension.mobileprovision OldApp_AppStore_Share_Extension + + - name: Decrypt AppStore profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt AppStore Notification Service profile + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore_Notification_Service.mobileprovision NewApp_AppStore_Notification_Service.mobileprovision.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt certificate + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output Certificates.p12 Certificates.p12.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Decrypt App Store Connect API key + run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output ios-fastlane-json-key.json ios-fastlane-json-key.json.gpg + env: + LARGE_SECRET_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + + - name: Set current App version in Env + run: echo "VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Get iOS native version + id: getIOSVersion + run: echo "IOS_VERSION=$(echo '${{ needs.prep.outputs.APP_VERSION }}' | tr '-' '.')" >> "$GITHUB_OUTPUT" + + - name: Build iOS HybridApp + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios build_hybrid + + - name: Upload release build to TestFlight + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + run: bundle exec fastlane ios upload_testflight_hybrid + env: + APPLE_CONTACT_EMAIL: ${{ secrets.APPLE_CONTACT_EMAIL }} + APPLE_CONTACT_PHONE: ${{ secrets.APPLE_CONTACT_PHONE }} + APPLE_DEMO_EMAIL: ${{ secrets.APPLE_DEMO_EMAIL }} + APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }} + + - 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=@${{ env.ipaPath }}" + env: + BROWSERSTACK: ${{ secrets.BROWSERSTACK }} + + - name: Upload iOS build artifact + if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: actions/upload-artifact@v4 + with: + name: ios-hybrid-build-artifact + path: ${{ env.ipaPath }} + + - name: Warn deployers if iOS production deploy failed + if: ${{ failure() && fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `πŸ’₯ iOS HybridApp production deploy failed. Please manually submit ${{ steps.getIOSVersion.outputs.IOS_VERSION }} in the . πŸ’₯`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + web: name: Build and deploy Web needs: prep @@ -415,7 +690,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, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] steps: - name: Checkout uses: actions/checkout@v4 @@ -425,23 +700,12 @@ jobs: with: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - # Build a version of iOS and Android HybridApp if we are deploying to staging - hybridApp: - runs-on: ubuntu-latest - needs: prep - if: ${{ github.ref == 'refs/heads/staging' }} - steps: - - name: 'Deploy HybridApp' - run: gh workflow run --repo Expensify/Mobile-Deploy deploy.yml -f force_build=true -f build_version="${{ needs.prep.outputs.APP_VERSION }}" - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - checkDeploymentSuccess: runs-on: ubuntu-latest 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, desktop, iOS, web] + needs: [buildAndroid, uploadAndroid, submitAndroid, android_hybrid, desktop, iOS, iOS_hybrid, web] if: ${{ always() }} steps: - name: Check deployment success on at least one platform @@ -457,8 +721,10 @@ jobs: isAtLeastOnePlatformDeployed="true" fi fi - + if [ "${{ needs.iOS.result }}" == "success" ] || \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] || \ + [ "${{ needs.android_hybrid.result }}" == "success" ] || \ [ "${{ needs.desktop.result }}" == "success" ] || \ [ "${{ needs.web.result }}" == "success" ]; then isAtLeastOnePlatformDeployed="true" @@ -471,6 +737,8 @@ jobs: run: | isAllPlatformsDeployed="false" if [ "${{ needs.iOS.result }}" == "success" ] && \ + [ "${{ needs.iOS_hybrid.result }}" == "success" ] && \ + [ "${{ needs.android_hybrid.result }}" == "success" ] && \ [ "${{ needs.desktop.result }}" == "success" ] && \ [ "${{ needs.web.result }}" == "success" ]; then isAllPlatformsDeployed="true" @@ -520,11 +788,13 @@ jobs: run: | gh release upload ${{ needs.prep.outputs.APP_VERSION }} --repo ${{ github.repository }} --clobber \ ./android-sourcemaps-artifact/index.android.bundle.map#android-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ - ./android-build-artifact/app-production-release.aab \ + ./android-aab-artifact/app-production-release.aab \ + ./android-hybrid-build-artifact/Expensify-release.aab#Android-HybridApp.aab \ ./desktop-staging-sourcemaps-artifact/desktop-staging-merged-source-map.js.map#desktop-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./desktop-staging-build-artifact/NewExpensify.dmg#NewExpensifyStaging.dmg \ ./ios-sourcemaps-artifact/main.jsbundle.map#ios-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./ios-build-artifact/New\ Expensify.ipa \ + ./ios-hybrid-build-artifact/Expensify.ipa#iOS-HybridApp.ipa \ ./web-staging-sourcemaps-artifact/web-staging-merged-source-map.js.map#web-staging-sourcemap-${{ needs.prep.outputs.APP_VERSION }} \ ./web-staging-build-tar-gz-artifact/webBuild.tar.gz#stagingWebBuild.tar.gz \ ./web-staging-build-zip-artifact/webBuild.zip#stagingWebBuild.zip @@ -605,7 +875,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, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, 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 @@ -659,11 +929,13 @@ jobs: postGithubComments: uses: ./.github/workflows/postDeployComments.yml if: ${{ always() && fromJSON(needs.checkDeploymentSuccess.outputs.IS_AT_LEAST_ONE_PLATFORM_DEPLOYED) }} - needs: [prep, buildAndroid, uploadAndroid, submitAndroid, desktop, iOS, web, checkDeploymentSuccess, createPrerelease, finalizeRelease] + needs: [prep, buildAndroid, uploadAndroid, submitAndroid, 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_hybrid: ${{ needs.android_hybrid.result }} ios: ${{ needs.iOS.result }} + ios_hybrid: ${{ needs.iOS_hybrid.result }} web: ${{ needs.web.result }} desktop: ${{ needs.desktop.result }} diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index b48c7b2175eb..c92ab83d1178 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -35,8 +35,16 @@ jobs: - name: Determine "baseline ref" (prev merge commit) id: getBaselineRef run: | - previous_merge=$(git rev-list --merges HEAD~1 | head -n 1) - git checkout "$previous_merge" + # Get the name of the current branch + current_branch=$(git rev-parse --abbrev-ref HEAD) + + if [ "$current_branch" = "main" ]; then + # On the main branch, find the previous merge commit + previous_merge=$(git rev-list --merges HEAD~1 | head -n 1) + else + # On a feature branch, find the common ancestor of the current branch and main + previous_merge=$(git merge-base HEAD main) + fi echo "$previous_merge" echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/postDeployComments.yml b/.github/workflows/postDeployComments.yml index 3893d3cf3f7c..ca138be0888b 100644 --- a/.github/workflows/postDeployComments.yml +++ b/.github/workflows/postDeployComments.yml @@ -15,10 +15,18 @@ on: description: Android deploy status required: true type: string + android_hybrid: + description: Android HybridApp deploy status + required: true + type: string ios: description: iOS deploy status required: true type: string + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: string web: description: Web deploy status required: true @@ -49,6 +57,15 @@ on: - failure - cancelled - skipped + android_hybrid: + description: Android HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped ios: description: iOS deploy status required: true @@ -58,6 +75,15 @@ on: - failure - cancelled - skipped + ios_hybrid: + description: iOS HybridApp deploy status + required: true + type: choice + options: + - success + - failure + - cancelled + - skipped web: description: Web deploy status required: true @@ -110,9 +136,11 @@ jobs: IS_PRODUCTION_DEPLOY: ${{ inputs.env == 'production' }} DEPLOY_VERSION: ${{ inputs.version }} GITHUB_TOKEN: ${{ github.token }} + ANDROID_HYBRID: ${{ inputs.android_hybrid }} ANDROID: ${{ inputs.android }} DESKTOP: ${{ inputs.desktop }} IOS: ${{ inputs.ios }} + IOS_HYBRID: ${{ inputs.ios_hybrid }} WEB: ${{ inputs.web }} DATE: ${{ inputs.date }} NOTE: ${{ inputs.note }} diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 672d468ed3b1..1bba3e96735a 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -92,7 +92,7 @@ jobs: uses: actions/download-artifact@v4 with: path: /tmp/artifacts - pattern: android-artifact-* + pattern: android-*-artifact merge-multiple: true - name: Log downloaded artifact paths diff --git a/.prettierignore b/.prettierignore index c4c88bd11d3e..b428978a1563 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,8 +18,6 @@ package-lock.json *.markdown # We need to modify the import here specifically, hence we disable prettier to get rid of the sorted imports src/libs/E2E/reactNativeLaunchingTest.ts -# Temporary while we keep react-compiler in our repo -lib/** # Automatically generated files src/libs/SearchParser/searchParser.js diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index dce724440adf..54fdd5681ae4 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -2,7 +2,7 @@ "applinks": { "details": [ { - "appIDs": ["368M544MTT.com.chat.expensify.chat"], + "appIDs": ["368M544MTT.com.chat.expensify.chat", "452835FXHF.com.expensify.expensifylite"], "components": [ { "/": "/r/*", @@ -105,6 +105,6 @@ ] }, "webcredentials": { - "apps": ["368M544MTT.com.chat.expensify.chat"] + "apps": ["368M544MTT.com.chat.expensify.chat", "452835FXHF.com.expensify.expensifylite"] } } diff --git a/Gemfile.lock b/Gemfile.lock index 00232570d5de..35920fc3e988 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,20 +20,20 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.948.0) - aws-sdk-core (3.199.0) + aws-partitions (1.979.0) + aws-sdk-core (3.209.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.87.0) - aws-sdk-core (~> 3, >= 3.199.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.154.0) - aws-sdk-core (~> 3, >= 3.199.0) + aws-sdk-kms (1.94.0) + aws-sdk-core (~> 3, >= 3.207.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.166.0) + aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) @@ -90,7 +90,7 @@ GEM ethon (0.16.0) ffi (>= 1.15.0) excon (0.111.0) - faraday (1.10.3) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -116,7 +116,7 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.3.1) fastlane (2.222.0) @@ -187,7 +187,7 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.0) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) @@ -208,14 +208,14 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.5) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.7.2) - jwt (2.8.2) + jwt (2.9.1) base64 mime-types (3.5.1) mime-types-data (~> 3.2015) @@ -241,8 +241,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.9) - strscan + rexml (3.3.7) rouge (2.0.7) ruby-macho (2.5.1) ruby2_keywords (0.0.5) @@ -256,7 +255,6 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -270,15 +268,15 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.24.0) + xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.2, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) diff --git a/README.md b/README.md index 4a691045e7c2..730e745e368a 100644 --- a/README.md +++ b/README.md @@ -274,7 +274,7 @@ web: `npm run symbolicate-release:web` - Perfetto UI (https://ui.perfetto.dev/) - Google Chrome's Tracing UI (chrome://tracing) ---- +---- # App Structure and Conventions diff --git a/__mocks__/@react-navigation/native/index.ts b/__mocks__/@react-navigation/native/index.ts index 5bcafdc1856c..55d19124e65e 100644 --- a/__mocks__/@react-navigation/native/index.ts +++ b/__mocks__/@react-navigation/native/index.ts @@ -16,9 +16,10 @@ const {triggerTransitionEnd, addListener} = isJestEnv addListener: () => {}, }; +const realOrMockedUseNavigation = isJestEnv ? realReactNavigation.useNavigation : {}; const useNavigation = () => ({ - ...realReactNavigation.useNavigation, - navigate: jest.fn(), + ...realOrMockedUseNavigation, + navigate: isJestEnv ? jest.fn() : () => {}, getState: () => ({ routes: [], }), @@ -30,17 +31,20 @@ type NativeNavigationMock = typeof ReactNavigation & { }; export * from '@react-navigation/core'; -const Link = realReactNavigation.Link; -const LinkingContext = realReactNavigation.LinkingContext; -const NavigationContainer = realReactNavigation.NavigationContainer; -const ServerContainer = realReactNavigation.ServerContainer; -const DarkTheme = realReactNavigation.DarkTheme; -const DefaultTheme = realReactNavigation.DefaultTheme; -const ThemeProvider = realReactNavigation.ThemeProvider; -const useLinkBuilder = realReactNavigation.useLinkBuilder; -const useLinkProps = realReactNavigation.useLinkProps; -const useLinkTo = realReactNavigation.useLinkTo; -const useScrollToTop = realReactNavigation.useScrollToTop; +const Link = isJestEnv ? realReactNavigation.Link : () => null; +const LinkingContext = isJestEnv ? realReactNavigation.LinkingContext : () => null; +const NavigationContainer = isJestEnv ? realReactNavigation.NavigationContainer : () => null; +const ServerContainer = isJestEnv ? realReactNavigation.ServerContainer : () => null; +const DarkTheme = isJestEnv ? realReactNavigation.DarkTheme : {}; +const DefaultTheme = isJestEnv ? realReactNavigation.DefaultTheme : {}; +const ThemeProvider = isJestEnv ? realReactNavigation.ThemeProvider : () => null; +const useLinkBuilder = isJestEnv ? realReactNavigation.useLinkBuilder : () => null; +const useLinkProps = isJestEnv ? realReactNavigation.useLinkProps : () => null; +const useLinkTo = isJestEnv ? realReactNavigation.useLinkTo : () => null; +const useScrollToTop = isJestEnv ? realReactNavigation.useScrollToTop : () => null; +const useRoute = isJestEnv ? realReactNavigation.useRoute : () => ({params: {}}); +const useFocusEffect = isJestEnv ? realReactNavigation.useFocusEffect : (callback: () => void) => callback(); + export { // Overriden modules useIsFocused, @@ -60,6 +64,8 @@ export { useLinkProps, useLinkTo, useScrollToTop, + useRoute, + useFocusEffect, }; export type {NativeNavigationMock}; diff --git a/android/app/build.gradle b/android/app/build.gradle index 472f8f52b87f..6f084f3659e3 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 1009005205 - versionName "9.0.52-5" + versionCode 1009005500 + versionName "9.0.55-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/android/app/src/main/java/com/expensify/chat/MainApplication.kt b/android/app/src/main/java/com/expensify/chat/MainApplication.kt index 2cc8b7780253..942304c80445 100644 --- a/android/app/src/main/java/com/expensify/chat/MainApplication.kt +++ b/android/app/src/main/java/com/expensify/chat/MainApplication.kt @@ -11,9 +11,11 @@ import com.expensify.chat.bootsplash.BootSplashPackage import com.expensify.chat.shortcutManagerModule.ShortcutManagerPackage import com.facebook.react.PackageList import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost import com.facebook.react.ReactNativeHost import com.facebook.react.ReactPackage import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.modules.i18nmanager.I18nUtil import com.facebook.soloader.SoLoader @@ -44,6 +46,9 @@ class MainApplication : MultiDexApplication(), ReactApplication { get() = BuildConfig.IS_HERMES_ENABLED }) + override val reactHost: ReactHost + get() = getDefaultReactHost(applicationContext, reactNativeHost) + override fun onCreate() { super.onCreate() ReactFontManager.getInstance().addCustomFont(this, "Expensify New Kansas", R.font.expensify_new_kansas) diff --git a/babel.config.js b/babel.config.js index 663eb29d5d2f..3f0fff03736d 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,11 +3,14 @@ require('dotenv').config(); const IS_E2E_TESTING = process.env.E2E_TESTING === 'true'; const ReactCompilerConfig = { - runtimeModule: 'react-compiler-runtime', + target: '18', environment: { enableTreatRefLikeIdentifiersAsRefs: true, }, + // We exclude 'tests' directory from compilation, but still compile components imported in test files. + sources: (filename) => !filename.includes('tests/') && !filename.includes('node_modules/'), }; + /** * Setting targets to node 20 to reduce JS bundle size * It is also recommended by babel: @@ -52,6 +55,8 @@ const webpack = { const metro = { presets: [require('@react-native/babel-preset')], plugins: [ + ['babel-plugin-react-compiler', ReactCompilerConfig], // must run first! + // This is needed due to a react-native bug: https://github.com/facebook/react-native/issues/29084#issuecomment-1030732709 // It is included in metro-react-native-babel-preset but needs to be before plugin-proposal-class-properties or FlatList will break '@babel/plugin-transform-flow-strip-types', @@ -154,11 +159,5 @@ module.exports = (api) => { const runningIn = api.caller((args = {}) => args.name); console.debug(' - running in: ', runningIn); - // don't include react-compiler in jest, because otherwise tests will fail - if (runningIn !== 'babel-jest') { - // must run first! - metro.plugins.unshift(['babel-plugin-react-compiler', ReactCompilerConfig]); - } - return ['metro', 'babel-jest'].includes(runningIn) ? metro : webpack; }; diff --git a/contributingGuides/BUGZERO_CHECKLIST.md b/contributingGuides/BUGZERO_CHECKLIST.md new file mode 100644 index 000000000000..00075620641c --- /dev/null +++ b/contributingGuides/BUGZERO_CHECKLIST.md @@ -0,0 +1,62 @@ +# BugZero Checklist: + +- [ ] **[Contributor]** Classify the bug: + +
+Bug classification + + +Source of bug: + - [ ] 1a. Result of the original design (eg. a case wasn't considered) + - [ ] 1b. Mistake during implementation + - [ ] 1c. Backend bug + - [ ] 1z. Other: + +Where bug was reported: + - [ ] 2a. Reported on production + - [ ] 2b. Reported on staging (deploy blocker) + - [ ] 2c. Reported on a PR + - [ ] 2z. Other: + +Who reported the bug: + - [ ] 3a. Expensify user + - [ ] 3b. Expensify employee + - [ ] 3c. Contributor + - [ ] 3d. QA + - [ ] 3z. Other: + +
+ +- [ ] **[Contributor]** The offending PR has been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake. + + Link to comment: + +- [ ] **[Contributor]** If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in [#expensify-open-source](https://app.slack.com/client/E047TPA624F/C01GTK53T8Q) has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner. + + Link to discussion: + +- [ ] **[Contributor]** If it was decided to create a regression test for the bug, please propose the [regression test](https://github.com/Expensify/App/blob/main/contributingGuides/REGRESSION_TEST_BEST_PRACTICES.md) steps using the template below to ensure the same bug will not reach production again. + +
+Regression Test Proposal Template + + +- [ ] **[BugZero Assignee]** Create a GH issue for creating/updating the regression test once above steps have been agreed upon. + + Link to issue: + +## Regression Test Proposal +### Precondition: + + +- + +### Test: + + +1. + +Do we agree πŸ‘ or πŸ‘Ž + + +
diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md index 917c3c007b28..dd913af1c497 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Configure-Quickbooks-Desktop.md @@ -7,6 +7,8 @@ Our new QuickBooks Desktop integration allows you to automate the import and exp # Step 1: Configure export settings The following steps will determine how data will be exported from Expensify to QuickBooks Desktop. +![Expensify export settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-export-settings.png){:width="100%"} + 1. In Expensify, hover over **Settings** and click **Workspaces**. 2. Select the Workspace you want to connect to QuickBooks Desktop. 3. Click the **Connections** tab. @@ -28,6 +30,8 @@ The following steps will determine how data will be exported from Expensify to Q The following steps help you determine how data will be imported from QuickBooks Online to Expensify: +![Expensify coding settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-coding-settings.png){:width="100%"} + 1. Click Import under the QuickBooks Online connection. 2. Review each of the following import settings: - **Chart of Accounts**: The Chart of Accounts is automatically imported from QuickBooks Desktop as categories. This cannot be amended. @@ -39,6 +43,8 @@ The following steps help you determine how data will be imported from QuickBooks The following steps help you determine the advanced settings for your connection, like auto-sync and employee invitation settings. +![Expensify advanced settings page for the QuickBooks Desktop integration](https://help.expensify.com/assets/images/quickbooks-desktop-advanced-settings.png){:width="100%"} + 1. Click **Advanced** under the QuickBooks Desktop connection. 2. **Enable or disable Auto-Sync**: If enabled, QuickBooks Desktop automatically communicates changes with Expensify to ensure that the data shared between the two systems is up to date. New report approvals/reimbursements will be synced during the next auto-sync period. diff --git a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md index 06f894ce7ef6..c832667080d5 100644 --- a/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md +++ b/docs/articles/expensify-classic/connections/quickbooks-desktop/Quickbooks-Desktop-Troubleshooting.md @@ -40,7 +40,13 @@ Generally, these errors indicate that there is a credentials issue. 4. Check that you have the correct permissions. 5. Log in to QuickBooks Desktop as an Admin (in single-user mode). 6. Go to **Edit** > **Preferences** > **Integrated Applications** > **Company Preferences**. -7. Select the Web Connector and click **Properties**. + +![Company Preferences page of QuickBooks Desktop](https://help.expensify.com/assets/images/quickbooks-desktop-company-preferences.png){:width="100%"} + +7. Select the Web Connector and click **Properties**. + +![Web Connector Properties page in QuickBooks Desktop](https://help.expensify.com/assets/images/quickbooks-desktop-access-rights.png){:width="100%"} + 8. Make sure that the "Allow this application to login automatically" checkbox is selected and click **OK**. 9. Close all windows in QuickBooks. @@ -98,6 +104,11 @@ Generally, this is the result of not having both the QuickBooks Web Connector an 1. Make sure that the Web Connector and QuickBooks Desktop Company File are both open. 2. In the Web Connector, check that the Last Status is β€œOk”. + +![QuickBooks Web Connector showing status "OK"](https://help.expensify.com/assets/images/quickbooks-desktop-web-connector.png){:width="100%"} + 3. Check the Report Comments in Expensify to confirm that the report has been successfully exported to QuickBooks Desktop. +![Expensify report showing the report was exported](https://help.expensify.com/assets/images/quickbooks-desktop-exported-report-comments.png){:width="100%"} + If these general troubleshooting steps don’t work, reach out to Concierge with your Expensify Report ID and a screenshot of your QuickBooks Web Connector. diff --git a/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md b/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md index eb35b1589db4..ff1b7fa00f1e 100644 --- a/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md +++ b/docs/articles/new-expensify/connections/xero/Connect-to-Xero.md @@ -5,10 +5,10 @@ order: 1 --- {% include info.html %} -To use the Xero connection, you must have a Xero account and an Expensify Collect plan. +You must have a Xero account and an Expensify Collect plan to use the Xero connection. {% include end-info.html %} -To set up your Xero connection, complete the 4 steps below. +To set up your Xero connection, complete the steps below. # Step 1: Connect Expensify to Xero @@ -29,68 +29,6 @@ To set up your Xero connection, complete the 4 steps below. ![The QuickBooks Online Connect button]({{site.url}}/assets/images/ExpensifyHelp-Xero-3.png){:width="100%"} -# Step 2: Configure import settings - -The following steps help you determine how data will be imported from Xero to Expensify. - -
    -
  1. Under the Accounting settings for your workspace, click Import under the Xero connection.
  2. -
  3. Select an option for each of the following settings to determine what information will be imported from Xero into Expensify:
  4. -
      -
    • Xero organization: Select which Xero organization your Expensify workspace is connected to. Each organization can only be connected to one workspace at a time.
    • -
    • Chart of Accounts: Your Xero chart of accounts and any accounts marked as β€œShow In Expense Claims” will be automatically imported into Expensify as Categories. This cannot be amended.
    • -
    • Tracking Categories: Choose whether to import your Xero categories for cost centers and regions as tags in Expensify.
    • -
    • Re-bill Customers: When enabled, Xero customer contacts are imported into Expensify as tags for expense tracking. After exporting to Xero, tagged billable expenses can be included on a sales invoice to your customer.
    • -
    • Taxes: Choose whether to import tax rates and tax defaults from Xero.
    • -
    -
- -# Step 3: Configure export settings -The following steps help you determine how data will be exported from Expensify to Xero. - -
    -
  1. Under the Accounting settings for your workspace, click Export under the Xero connection.
  2. -
  3. Review each of the following export settings:
  4. -
      -
    • Preferred Exporter: Choose whether to assign a Workspace Admin as the Preferred Exporter. Once selected, the Preferred Exporter automatically receives reports for export in their account to help automate the exporting process.
    • -
    -
-{% include info.html %} -- Other Workspace Admins will still be able to export to Xero. -- If you set different export accounts for individual company cards under your domain settings, then your Preferred Exporter must be a Domain Admin. -{% include end-info.html %} - -
    -
      -
    • Export Out-of-Pocket Expenses as: All out-of-pocket expenses will be exported as purchase bills. This cannot be amended.
    • -
    • Purchase Bill Date: Choose whether to use the date of last expense, export date, or submitted date.
    • -
    • Export invoices as: All invoices exported to Xero will be as a sales invoice. This cannot be amended.
    • -
    • Export company card expenses as: All company card expenses export to Xero as bank transactions. This cannot be amended.
    • -
    • Xero Bank Account: Select which bank account will be used to post bank transactions when non-reimbursable expenses are exported.
    • -
    -
- -# Step 4: Configure advanced settings - -The following steps help you determine the advanced settings for your connection, like auto-sync. - -
    -
  1. Under the Accounting settings for your workspace, click Advanced under the Xero connection.
  2. -
  3. Select an option for each of the following settings:
  4. -
      -
    • Auto-sync: Choose whether to enable Xero to automatically communicate changes with Expensify to ensure that the data shared between the two systems is up-to-date. New report approvals/reimbursements will be synced during the next auto-sync period. Once you’ve added a business bank account for ACH reimbursement, any reimbursable expenses will be sent to Xero automatically when the report is reimbursed. For non-reimbursable reports, Expensify automatically queues the report to export to Xero after it has completed the approval workflow in Expensify.
    • -
    • Set Purchase Bill Status: Choose the status of your purchase bills:
    • -
        -
      • Draft
      • -
      • Awaiting Approval
      • -
      • Awaiting Payment
      • -
      -
    • Sync Reimbursed Reports: Choose whether to enable report syncing for reimbursed expenses. If enabled, all reports that are marked as Paid in Xero will also show in Expensify as Paid. If enabled, you must also select the Xero account that reimbursements are coming out of, and Expensify will automatically create the payment in Xero.
    • -
    • Xero Bill Payment Account: If you enable Sync Reimbursed Reports, you must select the Xero Bill Payment account your reimbursements will come from.
    • -
    • Xero Invoice Collections Account: If you are exporting invoices from Expensify, select the invoice collection account that you want invoices to appear under once they are marked as paid.
    • -
    -
- {% include faq-begin.md %} **How do I disconnect Xero from Expensify?** @@ -99,7 +37,7 @@ The following steps help you determine the advanced settings for your connection 2. Scroll down and click **Workspaces** in the left menu. 3. Select the workspace you want to disconnect from Xero. 4. Click **Accounting** in the left menu. -5. Click the three dot menu icon to the right of Xero and select **Disconnect**. +5. Click the three-dot menu icon to the right of Xero and select **Disconnect**. 6. Click **Disconnect** to confirm. You will no longer see the imported options from Xero. diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png new file mode 100644 index 000000000000..53c637736c95 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_1.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png new file mode 100644 index 000000000000..92e607756de2 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_PayInvoice_2.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png new file mode 100644 index 000000000000..402afb86cc40 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice.png differ diff --git a/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png new file mode 100644 index 000000000000..7aeb0fdfb7c5 Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_OldDot_SendInvoice_02.png differ diff --git a/docs/assets/images/invoice-bulk-01.png b/docs/assets/images/invoice-bulk-01.png new file mode 100644 index 000000000000..1dbf7fa5088d Binary files /dev/null and b/docs/assets/images/invoice-bulk-01.png differ diff --git a/docs/assets/images/invoice-bulk-02.png b/docs/assets/images/invoice-bulk-02.png new file mode 100644 index 000000000000..82e388b0125f Binary files /dev/null and b/docs/assets/images/invoice-bulk-02.png differ diff --git a/docs/assets/images/invoice-bulk-03.png b/docs/assets/images/invoice-bulk-03.png new file mode 100644 index 000000000000..f51abec046b7 Binary files /dev/null and b/docs/assets/images/invoice-bulk-03.png differ diff --git a/docs/assets/images/invoice-bulk-04.png b/docs/assets/images/invoice-bulk-04.png new file mode 100644 index 000000000000..35e12a095ba6 Binary files /dev/null and b/docs/assets/images/invoice-bulk-04.png differ diff --git a/docs/assets/images/invoice-bulk-05.png b/docs/assets/images/invoice-bulk-05.png new file mode 100644 index 000000000000..c7044c259de2 Binary files /dev/null and b/docs/assets/images/invoice-bulk-05.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index a7d4d94adb5d..d3672618cfad 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -585,7 +585,8 @@ https://community.expensify.com/discussion/6699/faq-troubleshooting-known-bank-s https://community.expensify.com/discussion/4730/faq-expenses-are-exporting-to-the-wrong-accounts-whys-that,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Company-Card-Settings https://community.expensify.com/discussion/9000/how-to-integrate-with-deel,https://help.expensify.com/articles/expensify-classic/connections/Deel https://community.expensify.com/categories/expensify-classroom,https://use.expensify.com -https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription +https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Send-Receive-for-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Send-and-Receive-Payment-for-Invoices.md https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Bulk-Upload-Multiple-Invoices,https://help.expensify.com/articles/expensify-classic/articles/expensify-classic/expenses/Add-Invoices-in-Bulk -https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills \ No newline at end of file +https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills +https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription diff --git a/fastlane/Appfile b/fastlane/Appfile index 66955822aab7..43c8da9bddd5 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -3,3 +3,7 @@ apple_id("ios@expensify.com") # Your Apple email address itc_team_id("152696") # App Store Connect Team ID team_id("368M544MTT") # Developer Portal Team ID + +for_lane :build_hybrid, :build_unsigned_hybrid, :upload_testflight_hybrid do + app_identifier("com.expensify.expensifylite") +end diff --git a/fastlane/Fastfile b/fastlane/Fastfile index eed84acdc916..3c85aa5ff3d1 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -68,6 +68,23 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a production HybridApp AAB" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + gradle( + project_dir: '../Android', + task: "bundleRelease", + flags: "--refresh-dependencies", + properties: { + "android.injected.signing.store.file" => './upload-key.keystore', + "android.injected.signing.store.password" => ENV["ANDROID_UPLOAD_KEYSTORE_PASSWORD"], + "android.injected.signing.key.alias" => ENV["ANDROID_UPLOAD_KEYSTORE_ALIAS"], + "android.injected.signing.key.password" => ENV["ANDROID_UPLOAD_KEY_PASSWORD"], + } + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK" lane :build_local do ENV["ENVFILE"]=".env.production" @@ -80,6 +97,18 @@ platform :android do setGradleOutputsInEnv() end + desc "Generate a new local HybridApp APK" + lane :build_local_hybrid do + ENV["ENVFILE"]=".env.production" + gradle( + project_dir: '../Android', + task: 'assemble', + flavor: 'Production', + build_type: 'Release', + ) + setGradleOutputsInEnv() + end + desc "Generate a new local APK for e2e testing" lane :build_e2e do ENV["ENVFILE"]="tests/e2e/.env.e2e" @@ -151,6 +180,19 @@ platform :android do ) end + desc "Upload HybridApp to Google Play for internal testing" + lane :upload_google_play_internal_hybrid do + # Google is very unreliable, so we retry a few times + ENV["SUPPLY_UPLOAD_MAX_RETRIES"]="5" + upload_to_play_store( + package_name: "org.me.mobiexpensifyg", + json_key: './android-fastlane-json-key.json', + aab: ENV[KEY_GRADLE_AAB_PATH], + track: 'alpha', + rollout: '1.0' + ) + end + desc "Deploy app to Google Play production" lane :upload_google_play_production do # Google is very unreliable, so we retry a few times @@ -228,6 +270,37 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an iOS HybridApp production build" + lane :build_hybrid do + ENV["ENVFILE"]="../.env.production.hybridapp" + + setupIOSSigningCertificate() + + install_provisioning_profile( + path: "./OldApp_AppStore.mobileprovision" + ) + + install_provisioning_profile( + path: "./OldApp_AppStore_Share_Extension.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" => "(OldApp) AppStore", + "com.expensify.expensifylite.SmartScanExtension" => "(OldApp) AppStore: Share Extension" + } + } + ) + + setIOSBuildOutputsInEnv() + end + desc "Build an unsigned iOS production build" lane :build_unsigned do ENV["ENVFILE"]=".env.production" @@ -238,6 +311,16 @@ platform :ios do setIOSBuildOutputsInEnv() end + desc "Build an unsigned iOS HybridApp production build" + lane :build_unsigned_hybrid do + ENV["ENVFILE"]="../Mobile-Expensify/.env.production.hybridapp" + build_app( + workspace: "../Mobile-Expensify/iOS/Expensify.xcworkspace", + scheme: "Expensify" + ) + setIOSBuildOutputsInEnv() + end + desc "Build AdHoc app for testing" lane :build_adhoc do ENV["ENVFILE"]=".env.adhoc" @@ -316,6 +399,40 @@ platform :ios do ) end + desc "Upload HybridApp to TestFlight" + lane :upload_testflight_hybrid do + upload_to_testflight( + api_key_path: "./ios/ios-fastlane-json-key.json", + distribute_external: true, + notify_external_testers: true, + changelog: "Thank you for beta testing New Expensify, this version includes bug fixes and improvements.", + groups: ["Beta"], + demo_account_required: true, + beta_app_review_info: { + contact_email: ENV["APPLE_CONTACT_EMAIL"], + contact_first_name: "Andrew", + contact_last_name: "Gable", + contact_phone: ENV["APPLE_CONTACT_PHONE"], + demo_account_name: ENV["APPLE_DEMO_EMAIL"], + demo_account_password: ENV["APPLE_DEMO_PASSWORD"], + notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me' + 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above + 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify' + 4. Open the email and copy the 6-digit sign-in code provided within + 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field" + } + ) + + puts "dsym path: #{ENV[KEY_DSYM_PATH]}" + upload_symbols_to_crashlytics( + app_id: "1:1008697809946:ios:3ffad71f664f2886", + 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" + ) + end + desc "Submit app to App Store Review" lane :submit_for_review do deliver( diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 96baba0d4e87..b3ec8febb1df 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -681,7 +681,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\n"; + shellScript = "if [ \"$CONFIGURATION\" != \"DebugDevelopment\" ]; then\n \"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\nelse\n echo \"Skipping FullStory Asset Uploader phase for DebugDevelopment scheme.\"\nfi\n"; }; 5CF45ABA52C0BB0D7B9D139A /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index dc0ef2812031..5608c44823f4 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -88,11 +88,6 @@ - (NSURL *)bundleURL #endif } -- (BOOL)bridgelessEnabled -{ - return NO; -} - // This methods is needed to support the hardware keyboard shortcuts - (NSArray *)keyCommands { return [HardwareShortcuts sharedInstance].keyCommands; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 35057e6e33c1..11dc1c89be6d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.52 + 9.0.55 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.52.5 + 9.0.55.0 FullStory OrgId diff --git a/ios/NewExpensify/RCTBootSplash.h b/ios/NewExpensify/RCTBootSplash.h index 5dc3def635f2..f25f3e28f561 100644 --- a/ios/NewExpensify/RCTBootSplash.h +++ b/ios/NewExpensify/RCTBootSplash.h @@ -1,12 +1,4 @@ -// -// RCTBootSplash.h -// NewExpensify -// -// Created by Mathieu Acthernoene on 07/01/2022. -// - #import -#import @interface RCTBootSplash : NSObject diff --git a/ios/NewExpensify/RCTBootSplash.mm b/ios/NewExpensify/RCTBootSplash.mm index 3e4a086f07b1..ddb3f2d047ce 100644 --- a/ios/NewExpensify/RCTBootSplash.mm +++ b/ios/NewExpensify/RCTBootSplash.mm @@ -2,19 +2,16 @@ #import -#if RCT_NEW_ARCH_ENABLED #import #import -#else #import -#endif -static NSMutableArray *_resolveQueue = nil; +static RCTSurfaceHostingProxyRootView *_rootView = nil; + static UIView *_loadingView = nil; -static UIView *_rootView = nil; -static float _duration = 0; +static NSMutableArray *_resolveQueue = [[NSMutableArray alloc] init]; +static bool _fade = false; static bool _nativeHidden = false; -static bool _transitioning = false; @implementation RCTBootSplash @@ -24,14 +21,18 @@ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } ++ (BOOL)requiresMainQueueSetup { + return NO; +} + + (void)invalidateBootSplash { _resolveQueue = nil; _rootView = nil; _nativeHidden = false; } -+ (bool)isLoadingViewHidden { - return _loadingView == nil || [_loadingView isHidden]; ++ (bool)isLoadingViewVisible { + return _loadingView != nil && ![_loadingView isHidden]; } + (bool)hasResolveQueue { @@ -41,7 +42,7 @@ + (bool)hasResolveQueue { + (void)clearResolveQueue { if (![self hasResolveQueue]) return; - + while ([_resolveQueue count] > 0) { RCTPromiseResolveBlock resolve = [_resolveQueue objectAtIndex:0]; [_resolveQueue removeObjectAtIndex:0]; @@ -49,19 +50,15 @@ + (void)clearResolveQueue { } } -+ (void)hideLoadingView { - if ([self isLoadingViewHidden]) ++ (void)hideAndClearPromiseQueue { + if (![self isLoadingViewVisible]) { return [RCTBootSplash clearResolveQueue]; + } - if (_duration > 0) { + if (_fade) { dispatch_async(dispatch_get_main_queue(), ^{ - _transitioning = true; - - if (_rootView == nil) - return; - [UIView transitionWithView:_rootView - duration:_duration / 1000.0 + duration:0.250 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ _loadingView.hidden = YES; @@ -70,7 +67,6 @@ + (void)hideLoadingView { [_loadingView removeFromSuperview]; _loadingView = nil; - _transitioning = false; return [RCTBootSplash clearResolveQueue]; }]; }); @@ -85,30 +81,9 @@ + (void)hideLoadingView { + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName rootView:(UIView * _Nullable)rootView { - if (rootView == nil -#ifdef RCT_NEW_ARCH_ENABLED - || ![rootView isKindOfClass:[RCTSurfaceHostingProxyRootView class]] -#else - || ![rootView isKindOfClass:[RCTRootView class]] -#endif - || _rootView != nil - || [self hasResolveQueue] // hide has already been called, abort init - || RCTRunningInAppExtension()) + if (RCTRunningInAppExtension()) { return; - -#ifdef RCT_NEW_ARCH_ENABLED - RCTSurfaceHostingProxyRootView *proxy = (RCTSurfaceHostingProxyRootView *)rootView; - _rootView = (RCTSurfaceHostingView *)proxy.surface.view; -#else - _rootView = (RCTRootView *)rootView; -#endif - - UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil]; - - _loadingView = [[storyboard instantiateInitialViewController] view]; - _loadingView.hidden = NO; - - [_rootView addSubview:_loadingView]; + } [NSTimer scheduledTimerWithTimeInterval:0.35 repeats:NO @@ -117,19 +92,35 @@ + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName _nativeHidden = true; // hide has been called before native launch screen fade out - if ([self hasResolveQueue]) - [self hideLoadingView]; + if ([_resolveQueue count] > 0) { + [self hideAndClearPromiseQueue]; + } }]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onJavaScriptDidLoad) - name:RCTJavaScriptDidLoadNotification - object:nil]; + if (rootView != nil) { + _rootView = (RCTSurfaceHostingProxyRootView *)rootView; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(onJavaScriptDidFailToLoad) - name:RCTJavaScriptDidFailToLoadNotification - object:nil]; + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil]; + + _loadingView = [[storyboard instantiateInitialViewController] view]; + _loadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _loadingView.frame = _rootView.bounds; + _loadingView.center = (CGPoint){CGRectGetMidX(_rootView.bounds), CGRectGetMidY(_rootView.bounds)}; + _loadingView.hidden = NO; + + [_rootView disableActivityIndicatorAutoHide:YES]; + [_rootView setLoadingView:_loadingView]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onJavaScriptDidLoad) + name:RCTJavaScriptDidLoadNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(onJavaScriptDidFailToLoad) + name:RCTJavaScriptDidFailToLoadNotification + object:nil]; + } } + (void)onJavaScriptDidLoad { @@ -137,50 +128,51 @@ + (void)onJavaScriptDidLoad { } + (void)onJavaScriptDidFailToLoad { - [self hideLoadingView]; + [self hideAndClearPromiseQueue]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)hide:(double)duration - resolve:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject { - if (_resolveQueue == nil) - _resolveQueue = [[NSMutableArray alloc] init]; +- (NSDictionary *)constantsToExport { + UIWindow *window = RCTKeyWindow(); + __block bool darkModeEnabled = false; - [_resolveQueue addObject:resolve]; + RCTUnsafeExecuteOnMainQueueSync(^{ + darkModeEnabled = window != nil && window.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark; + }); - if ([RCTBootSplash isLoadingViewHidden] || RCTRunningInAppExtension()) - return [RCTBootSplash clearResolveQueue]; + return @{ + @"darkModeEnabled": @(darkModeEnabled) + }; +} + +- (void)hideImpl:(BOOL)fade + resolve:(RCTPromiseResolveBlock)resolve { + if (_resolveQueue == nil) + _resolveQueue = [[NSMutableArray alloc] init]; + + [_resolveQueue addObject:resolve]; + + if (![RCTBootSplash isLoadingViewVisible] || RCTRunningInAppExtension()) + return [RCTBootSplash clearResolveQueue]; - _duration = lroundf((float)duration); + _fade = fade; - if (_nativeHidden) - return [RCTBootSplash hideLoadingView]; + if (_nativeHidden) + return [RCTBootSplash hideAndClearPromiseQueue]; } -- (void)getVisibilityStatus:(RCTPromiseResolveBlock)resolve - reject:(RCTPromiseRejectBlock)reject { - if ([RCTBootSplash isLoadingViewHidden]) - return resolve(@"hidden"); - else if (_transitioning) - return resolve(@"transitioning"); - else - return resolve(@"visible"); +- (void)isVisibleImpl:(RCTPromiseResolveBlock)resolve { + resolve(@([RCTBootSplash isLoadingViewVisible])); } -RCT_REMAP_METHOD(hide, - resolve:(RCTPromiseResolveBlock)resolve - rejecte:(RCTPromiseRejectBlock)reject) { - [self hide:0 - resolve:resolve - reject:reject]; +RCT_EXPORT_METHOD(hide:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self hideImpl:0 resolve:resolve]; } -RCT_REMAP_METHOD(getVisibilityStatus, - getVisibilityStatusWithResolve:(RCTPromiseResolveBlock)resolve - rejecte:(RCTPromiseRejectBlock)reject) { - [self getVisibilityStatus:resolve - reject:reject]; +RCT_EXPORT_METHOD(isVisible:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) { + [self isVisibleImpl:resolve]; } @end diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f1d0ef296b54..eef74d4f127c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.52 + 9.0.55 CFBundleSignature ???? CFBundleVersion - 9.0.52.5 + 9.0.55.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 3efdcd69ae04..33f6517f0329 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.52 + 9.0.55 CFBundleVersion - 9.0.52.5 + 9.0.55.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift index e489cb368d17..b588c6be1d0f 100644 --- a/ios/NotificationServiceExtension/NotificationService.swift +++ b/ios/NotificationServiceExtension/NotificationService.swift @@ -8,12 +8,18 @@ import AirshipServiceExtension import os.log import Intents +import AppLogs class NotificationService: UANotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? let log = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "com.expensify.chat.dev.NotificationServiceExtension", category: "NotificationService") + let appLogs: AppLogs = .init() + + deinit { + appLogs.forwardLogsTo(appGroup: "group.com.expensify.new") + } override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { os_log("[NotificationService] didReceive() - received notification", log: log) @@ -42,7 +48,7 @@ class NotificationService: UANotificationServiceExtension { do { notificationData = try parsePayload(notificationContent: notificationContent) } catch ExpError.runtimeError(let errorMessage) { - os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%@'", log: log, type: .error, errorMessage) + os_log("[NotificationService] configureCommunicationNotification() - couldn't parse the payload '%{public}@'", log: log, type: .error, errorMessage) contentHandler(notificationContent) return } catch { @@ -212,7 +218,7 @@ class NotificationService: UANotificationServiceExtension { let data = try Data(contentsOf: url) return INImage(imageData: data) } catch { - os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %@", log: self.log, type: .error, reportActionID) + os_log("[NotificationService] fetchINImage() - failed to fetch avatar. reportActionID: %{public}@", log: self.log, type: .error, reportActionID) return nil } } diff --git a/ios/Podfile b/ios/Podfile index e807089c26b9..4d139711ef01 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -119,6 +119,7 @@ end target 'NotificationServiceExtension' do pod 'AirshipServiceExtension' + pod 'AppLogs', :path => '../node_modules/react-native-app-logs/AppLogsPod' end pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz' \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1242ab7a5a39..9a706cc4e8aa 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,6 +26,7 @@ PODS: - AppAuth/Core (1.7.5) - AppAuth/ExternalUserAgent (1.7.5): - AppAuth/Core + - AppLogs (0.1.0) - boost (1.84.0) - DoubleConversion (1.1.6) - EXAV (14.0.7): @@ -1564,6 +1565,27 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-app-logs (0.3.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-blob-util (0.19.4): - DoubleConversion - glog @@ -2373,7 +2395,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.164): + - RNLiveMarkdown (0.1.176): - DoubleConversion - glog - hermes-engine @@ -2393,9 +2415,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.164) + - RNLiveMarkdown/newarch (= 0.1.176) - Yoga - - RNLiveMarkdown/newarch (0.1.164): + - RNLiveMarkdown/newarch (0.1.176): - DoubleConversion - glog - hermes-engine @@ -2702,6 +2724,7 @@ PODS: DEPENDENCIES: - AirshipServiceExtension + - AppLogs (from `../node_modules/react-native-app-logs/AppLogsPod`) - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) @@ -2751,6 +2774,7 @@ DEPENDENCIES: - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) - "react-native-airship (from `../node_modules/@ua/react-native-airship`)" + - react-native-app-logs (from `../node_modules/react-native-app-logs`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - react-native-config (from `../node_modules/react-native-config`) @@ -2864,6 +2888,8 @@ SPEC REPOS: - Turf EXTERNAL SOURCES: + AppLogs: + :path: "../node_modules/react-native-app-logs/AppLogsPod" boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" DoubleConversion: @@ -2959,6 +2985,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" react-native-airship: :path: "../node_modules/@ua/react-native-airship" + react-native-app-logs: + :path: "../node_modules/react-native-app-logs" react-native-blob-util: :path: "../node_modules/react-native-blob-util" react-native-cameraroll: @@ -3109,6 +3137,7 @@ SPEC CHECKSUMS: AirshipFrameworkProxy: dbd862dc6fb21b13e8b196458d626123e2a43a50 AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa + AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 boost: 26992d1adf73c1c7676360643e687aee6dda994b DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 EXAV: afa491e598334bbbb92a92a2f4dd33d7149ad37f @@ -3184,6 +3213,7 @@ SPEC CHECKSUMS: React-Mapbuffer: 1c08607305558666fd16678b85ef135e455d5c96 React-microtasksnativemodule: f13f03163b6a5ec66665dfe80a0df4468bb766a6 react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc + react-native-app-logs: b8a104816aafc78cd0965e923452de88dcf8ec67 react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44 react-native-cameraroll: 478a0c1fcdd39f08f6ac272b7ed06e92b2c7c129 react-native-config: 742a9e0a378a78d0eaff1fb3477d8c0ae222eb51 @@ -3242,7 +3272,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 8781e2529230a1bc3ea8d75e5c3cd071b6c6aed7 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: b2bd97a6f1206be16cf6536c092fe39f986aca34 + RNLiveMarkdown: 0b8756147a5e8eeea98d3e1187c0c27d5a96d1ff RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 460d6ff97ae49c7d5708c3212c6521697c36a0c4 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 @@ -3261,6 +3291,6 @@ SPEC CHECKSUMS: VisionCamera: c6c8aa4b028501fc87644550fbc35a537d4da3fb Yoga: a1d7895431387402a674fd0d1c04ec85e87909b8 -PODFILE CHECKSUM: a07e55247056ec5d84d1af31d694506efff3cfe2 +PODFILE CHECKSUM: 15e2f095b9c80d658459723edf84005a6867debf COCOAPODS: 1.15.2 diff --git a/jest/setup.ts b/jest/setup.ts index 6901ad3c66f3..7dbe91c32fda 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import '@shopify/flash-list/jestSetup'; +import type * as RNAppLogs from 'react-native-app-logs'; import 'react-native-gesture-handler/jestSetup'; import type * as RNKeyboardController from 'react-native-keyboard-controller'; import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; @@ -75,6 +76,8 @@ jest.mock('react-native-reanimated', () => ({ jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')); +jest.mock('react-native-app-logs', () => require('react-native-app-logs/jest')); + jest.mock('@src/libs/actions/Timing', () => ({ start: jest.fn(), end: jest.fn(), diff --git a/lib/react-compiler-runtime/index.js b/lib/react-compiler-runtime/index.js deleted file mode 100644 index 54e88d2b703a..000000000000 --- a/lib/react-compiler-runtime/index.js +++ /dev/null @@ -1,21 +0,0 @@ -// lib/react-compiler-runtime.js -const $empty = Symbol.for("react.memo_cache_sentinel"); -const React = require('react'); -/** - * DANGER: this hook is NEVER meant to be called directly! - * - * Note that this is a temporary userspace implementation of this function - * from React 19. It is not as efficient and may invalidate more frequently - * than the official API. Better to upgrade to React 19 as soon as we can. - **/ -export function c(size) { - return React.useState(() => { - const $ = new Array(size); - for (let ii = 0; ii < size; ii++) { - $[ii] = $empty; - } - // @ts-ignore - $[$empty] = true; - return $; - })[0]; -} diff --git a/lib/react-compiler-runtime/package.json b/lib/react-compiler-runtime/package.json deleted file mode 100644 index 3a0323538b6e..000000000000 --- a/lib/react-compiler-runtime/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "react-compiler-runtime", - "version": "0.0.1", - "description": "Runtime for React Compiler", - "license": "MIT", - "main": "index.js", - "dependencies": { - "react": "18.3.1" - } -} diff --git a/package-lock.json b/package-lock.json index a7966885b9d8..fbdba9487652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.52-5", + "version": "9.0.55-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.52-5", + "version": "9.0.55-0", "hasInstallScript": true, "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.164", + "@expensify/react-native-live-markdown": "0.1.176", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -77,6 +77,7 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", + "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", @@ -203,7 +204,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", + "babel-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^4.0.0", @@ -224,7 +225,7 @@ "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-lodash": "^7.4.0", - "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", + "eslint-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -247,8 +248,8 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", - "react-compiler-runtime": "file:./lib/react-compiler-runtime", + "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", + "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.3.1", @@ -278,14 +279,6 @@ "npm": "10.8.2" } }, - "lib/react-compiler-runtime": { - "version": "0.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "react": "18.3.1" - } - }, "node_modules/@actions/core": { "version": "1.10.0", "dev": true, @@ -3637,9 +3630,10 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.164", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.164.tgz", - "integrity": "sha512-x1/Oa+I1AI82xWEFYd2kSkSj4rZ1q2JG4aEDomUHSqcNjuQetQPw9kVFN5DaLHt0Iu0iKEUrXIhy5LpMSHJQLg==", + "version": "0.1.176", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.176.tgz", + "integrity": "sha512-0IS0Rfl0qYqrE2V8jsVX58c4K/zxeNC7o1CAL9Xu+HTbTtD58Yu5gOOwp5AljkS2qdPR86swGRZyLXGkGRKkPg==", + "license": "MIT", "workspaces": [ "parser", "example", @@ -18344,9 +18338,10 @@ } }, "node_modules/babel-plugin-react-compiler": { - "version": "0.0.0-experimental-334f00b-20240725", + "version": "19.0.0-beta-8a03594-20241020", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-8a03594-20241020.tgz", + "integrity": "sha512-Wk0748DZzQEmjkEN4SbBujM5al4q5TfRBapA32ax0AID/Yek3emS+eyCvPvb4zPddYJTAF4LaJNLt8uHYfdKAQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/generator": "7.2.0", "@babel/types": "^7.19.0", @@ -23530,9 +23525,10 @@ } }, "node_modules/eslint-plugin-react-compiler": { - "version": "0.0.0-experimental-9ed098e-20240725", + "version": "19.0.0-beta-8a03594-20241020", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.0.0-beta-8a03594-20241020.tgz", + "integrity": "sha512-bYg1COih1s3r14IV/AKdQs/SN7CQmNI0ZaMtPdgZ6gp1S1Q/KGP9P43w7R6dHJ4wYpuMBvekNJHQdVu+x6UM+A==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", @@ -34075,9 +34071,10 @@ } }, "node_modules/react-compiler-healthcheck": { - "version": "0.0.0-experimental-b130d5f-20240625", + "version": "19.0.0-beta-8a03594-20241020", + "resolved": "https://registry.npmjs.org/react-compiler-healthcheck/-/react-compiler-healthcheck-19.0.0-beta-8a03594-20241020.tgz", + "integrity": "sha512-wupgZ4fASQ+oRI88V6QIERKCHZUo6322LXlH8EotsWQDc8c4EXgPdkZHO/zH+zDh4Np4qZM36bFbZgHPXtI0FA==", "dev": true, - "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", @@ -34160,8 +34157,13 @@ } }, "node_modules/react-compiler-runtime": { - "resolved": "lib/react-compiler-runtime", - "link": true + "version": "19.0.0-beta-8a03594-20241020", + "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.0.0-beta-8a03594-20241020.tgz", + "integrity": "sha512-YWl8SjxsWGU1dpxHvWS0vxTkpeLXTZ/Y7IVzwZGj6yAfXOEie1MduuAR0TFiGeV0RxFLp5jKUIWl+ZglN4dMQw==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0 || ^19.0.0" + } }, "node_modules/react-content-loader": { "version": "7.0.0", @@ -34400,6 +34402,18 @@ "prop-types": "^15.7.2" } }, + "node_modules/react-native-app-logs": { + "version": "0.3.1", + "resolved": "git+ssh://git@github.com/margelo/react-native-app-logs.git#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", + "integrity": "sha512-GFZFbUe9bUIbuH2zTAS7JAXCAIYnyf4cTnsz6pSzYCl3F+nF+O3fRa5ZM8P7zr+wTG7fZoVs0b6XFfcFUcxY2A==", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-blob-util": { "version": "0.19.4", "license": "MIT", diff --git a/package.json b/package.json index 010aaf7ce235..9e24776bbf42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.52-5", + "version": "9.0.55-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.", @@ -67,7 +67,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.164", + "@expensify/react-native-live-markdown": "0.1.176", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -133,6 +133,7 @@ "react-map-gl": "^7.1.3", "react-native": "0.75.2", "react-native-android-location-enabler": "^2.0.1", + "react-native-app-logs": "git+https://github.com/margelo/react-native-app-logs#7e9c311bffdc6a9eeb69d90d30ead47e01c3552a", "react-native-blob-util": "0.19.4", "react-native-collapsible": "^1.6.2", "react-native-config": "1.5.3", @@ -259,7 +260,7 @@ "babel-jest": "29.4.1", "babel-loader": "^9.1.3", "babel-plugin-module-resolver": "^5.0.0", - "babel-plugin-react-compiler": "0.0.0-experimental-334f00b-20240725", + "babel-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020", "babel-plugin-react-native-web": "^0.18.7", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^4.0.0", @@ -280,7 +281,7 @@ "eslint-plugin-jest": "^28.6.0", "eslint-plugin-jsdoc": "^46.2.6", "eslint-plugin-lodash": "^7.4.0", - "eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725", + "eslint-plugin-react-compiler": "^19.0.0-beta-8a03594-20241020", "eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", @@ -303,8 +304,8 @@ "portfinder": "^1.0.28", "prettier": "^2.8.8", "pusher-js-mock": "^0.3.3", - "react-compiler-healthcheck": "^0.0.0-experimental-ab3118d-20240725", - "react-compiler-runtime": "file:./lib/react-compiler-runtime", + "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020", + "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020", "react-is": "^18.3.1", "react-native-clean-project": "^4.0.0-alpha4.0", "react-test-renderer": "18.3.1", diff --git a/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch b/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch new file mode 100644 index 000000000000..54ae4d9a1c58 --- /dev/null +++ b/patches/@react-native-firebase+app+12.9.3+002+bridgeless.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js b/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js +index 03f001c..358c795 100644 +--- a/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js ++++ b/node_modules/@react-native-firebase/app/lib/internal/registry/nativeModule.js +@@ -65,7 +65,7 @@ function nativeModuleWrapped(namespace, NativeModule, argToPrepend) { + return NativeModule; + } + +- const properties = Object.keys(NativeModule); ++ const properties = [...Object.keys(Object.getPrototypeOf(NativeModule)), ...Object.keys(NativeModule)]; + + for (let i = 0, len = properties.length; i < len; i++) { + const property = properties[i]; diff --git a/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch b/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch new file mode 100644 index 000000000000..b840e3da7b12 --- /dev/null +++ b/patches/@rnmapbox+maps+10.1.30+001+bridgeless.patch @@ -0,0 +1,83 @@ +diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt +index 5bebc1b..80a4be4 100644 +--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt ++++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/styles/sources/RNMBXRasterSourceManager.kt +@@ -5,6 +5,8 @@ import com.facebook.react.bridge.ReactApplicationContext + import com.facebook.react.uimanager.ThemedReactContext + import com.facebook.react.uimanager.annotations.ReactProp + import com.facebook.react.viewmanagers.RNMBXRasterSourceManagerInterface ++import com.rnmapbox.rnmbx.events.constants.EventKeys ++import com.rnmapbox.rnmbx.events.constants.eventMapOf + import javax.annotation.Nonnull + + class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext) : +@@ -26,7 +28,10 @@ class RNMBXRasterSourceManager(reactApplicationContext: ReactApplicationContext) + } + + override fun customEvents(): Map? { +- return null ++ return eventMapOf( ++ EventKeys.RASTER_SOURCE_LAYER_CLICK to "onMapboxRasterSourcePress", ++ EventKeys.MAP_ANDROID_CALLBACK to "onAndroidCallback" ++ ) + } + + companion object { +diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt +index d059b2c..3882f1e 100644 +--- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt ++++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/events/constants/EventKeys.kt +@@ -4,35 +4,37 @@ private fun ns(name: String): String { + val namespace = "rct.mapbox" + return String.format("%s.%s", namespace, name) + } ++ + enum class EventKeys(val value: String) { + // map events +- MAP_CLICK(ns("map.press")), +- MAP_LONG_CLICK(ns("map.longpress")), +- MAP_ONCHANGE(ns("map.change")), +- MAP_ON_LOCATION_CHANGE(ns("map.location.change")), +- MAP_ANDROID_CALLBACK(ns("map.androidcallback")), +- MAP_USER_TRACKING_MODE_CHANGE(ns("map.usertrackingmodechange")), ++ MAP_CLICK("topPress"), ++ MAP_LONG_CLICK("topLongPress"), ++ MAP_ONCHANGE("topMapChange"), ++ MAP_ON_LOCATION_CHANGE("topLocationChange"), ++ MAP_ANDROID_CALLBACK("topAndroidCallback"), ++ MAP_USER_TRACKING_MODE_CHANGE("topUserTrackingModeChange"), + + // point annotation events +- POINT_ANNOTATION_SELECTED(ns("pointannotation.selected")), +- POINT_ANNOTATION_DESELECTED(ns("pointannotation.deselected")), +- POINT_ANNOTATION_DRAG_START(ns("pointannotation.dragstart")), +- POINT_ANNOTATION_DRAG(ns("pointannotation.drag")), +- POINT_ANNOTATION_DRAG_END(ns("pointannotation.dragend")), ++ POINT_ANNOTATION_SELECTED("topMapboxPointAnnotationSelected"), ++ POINT_ANNOTATION_DESELECTED("topMapboxPointAnnotationDeselected"), ++ POINT_ANNOTATION_DRAG_START("topMapboxPointAnnotationDragStart"), ++ POINT_ANNOTATION_DRAG("topMapboxPointAnnotationDrag"), ++ POINT_ANNOTATION_DRAG_END("topMapboxPointAnnotationDragEnd"), + + // source events +- SHAPE_SOURCE_LAYER_CLICK(ns("shapesource.layer.pressed")), +- VECTOR_SOURCE_LAYER_CLICK(ns("vectorsource.layer.pressed")), +- RASTER_SOURCE_LAYER_CLICK(ns("rastersource.layer.pressed")), ++ SHAPE_SOURCE_LAYER_CLICK("topMapboxShapeSourcePress"), ++ VECTOR_SOURCE_LAYER_CLICK("topMapboxVectorSourcePress"), ++ RASTER_SOURCE_LAYER_CLICK("topMapboxRasterSourcePress"), + + // images event +- IMAGES_MISSING(ns("images.missing")), ++ IMAGES_MISSING("topImageMissing"), + + // location events ++ // TODO: not sure about this one since it is not registered anywhere + USER_LOCATION_UPDATE(ns("user.location.update")), + + // viewport events +- VIEWPORT_STATUS_CHANGE(ns("viewport.statuschange")) ++ VIEWPORT_STATUS_CHANGE("topStatusChanged") + } + + fun eventMapOf(vararg values: Pair): Map { diff --git a/patches/lottie-react-native+6.5.1.patch b/patches/lottie-react-native+6.5.1+001+recycling.patch similarity index 100% rename from patches/lottie-react-native+6.5.1.patch rename to patches/lottie-react-native+6.5.1+001+recycling.patch diff --git a/patches/lottie-react-native+6.5.1+002+bridgeless.patch b/patches/lottie-react-native+6.5.1+002+bridgeless.patch new file mode 100644 index 000000000000..854d26f9beb9 --- /dev/null +++ b/patches/lottie-react-native+6.5.1+002+bridgeless.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt +index aa538d3..0185eaf 100644 +--- a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt ++++ b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationFailureEvent.kt +@@ -21,6 +21,6 @@ constructor(surfaceId: Int, viewId: Int, private val error: Throwable) : + } + + companion object { +- const val EVENT_NAME = "topAnimationFailureEvent" ++ const val EVENT_NAME = "topAnimationFailure" + } + } +\ No newline at end of file +diff --git a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt +index f17cff9..4ebe3ba 100644 +--- a/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt ++++ b/node_modules/lottie-react-native/android/src/main/java/com/airbnb/android/react/lottie/OnAnimationLoadedEvent.kt +@@ -16,6 +16,6 @@ class OnAnimationLoadedEvent constructor(surfaceId: Int, viewId: Int) : + } + + companion object { +- const val EVENT_NAME = "topAnimationLoadedEvent" ++ const val EVENT_NAME = "topAnimationLoaded" + } + } diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch similarity index 66% rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch index d7c02701a636..03b386587338 100644 --- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+001+initial.patch +++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+001+initial.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index b427385..4bf23db 100755 +index 5a4060d..460339b 100755 --- a/node_modules/react-compiler-healthcheck/dist/index.js +++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -69154,7 +69154,7 @@ var reactCompilerCheck = { +@@ -56969,7 +56969,7 @@ var reactCompilerCheck = { compile(source, path); } }, @@ -11,11 +11,11 @@ index b427385..4bf23db 100755 const totalComponents = SucessfulCompilation.length + countUniqueLocInEvents(OtherFailures) + -@@ -69164,6 +69164,50 @@ var reactCompilerCheck = { +@@ -56979,6 +56979,50 @@ var reactCompilerCheck = { `Successfully compiled ${SucessfulCompilation.length} out of ${totalComponents} components.` ) ); -+ ++ + if (verbose) { + for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) { + const filename = compilation.fnLoc?.filename; @@ -38,33 +38,33 @@ index b427385..4bf23db 100755 + if (compilation.kind === "CompileError") { + const { reason, severity, loc } = compilation.detail; + -+ const lnNo = loc.start?.line; -+ const colNo = loc.start?.column; ++ const lnNo = loc.start?.line; ++ const colNo = loc.start?.column; + -+ const isTodo = severity === ErrorSeverity.Todo; ++ const isTodo = severity === ErrorSeverity.Todo; + -+ console.log( -+ chalk[isTodo ? 'yellow' : 'red']( -+ `Failed to compile ${ -+ filename -+ }${ -+ lnNo !== undefined ? `:${lnNo}${ -+ colNo !== undefined ? `:${colNo}` : "" -+ }.` : "" -+ }` -+ ), -+ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "") -+ ); -+ console.log("\n"); ++ console.log( ++ chalk[isTodo ? 'yellow' : 'red']( ++ `Failed to compile ${ ++ filename ++ }${ ++ lnNo !== undefined ? `:${lnNo}${ ++ colNo !== undefined ? `:${colNo}` : "" ++ }.` : "" ++ }` ++ ), ++ chalk[isTodo ? 'yellow' : 'red'](reason? `Reason: ${reason}` : "") ++ ); ++ console.log("\n"); + } + } + } }, }; const JsFileExtensionRE = /(js|ts|jsx|tsx)$/; -@@ -69200,9 +69244,16 @@ function main() { - type: "string", - default: "**/+(*.{js,mjs,jsx,ts,tsx}|package.json)", +@@ -57015,9 +57059,16 @@ function main() { + type: 'string', + default: '**/+(*.{js,mjs,jsx,ts,tsx}|package.json)', }) + .option('verbose', { + description: 'run with verbose logging', @@ -73,13 +73,13 @@ index b427385..4bf23db 100755 + alias: 'v', + }) .parseSync(); - const spinner = ora("Checking").start(); + const spinner = ora('Checking').start(); let src = argv.src; + let verbose = argv.verbose; const globOptions = { onlyFiles: true, ignore: [ -@@ -69222,7 +69273,7 @@ function main() { +@@ -57037,7 +57088,7 @@ function main() { libraryCompatCheck.run(source, path); } spinner.stop(); diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch similarity index 65% rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch index 6caa4ad4c373..8ae46e379619 100644 --- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+002+enable-ref-identifiers.patch +++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+002+enable-ref-identifiers.patch @@ -1,28 +1,28 @@ diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index 4bf23db..fa2ab22 100755 +index 460339b..17b0f96 100755 --- a/node_modules/react-compiler-healthcheck/dist/index.js +++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -69088,6 +69088,9 @@ const COMPILER_OPTIONS = { - compilationMode: "infer", - panicThreshold: "critical_errors", - logger: logger, +@@ -56902,6 +56902,9 @@ const COMPILER_OPTIONS = { + noEmit: true, + compilationMode: 'infer', + panicThreshold: 'critical_errors', + environment: { + enableTreatRefLikeIdentifiersAsRefs: true, + }, + logger: logger, }; function isActionableDiagnostic(detail) { - switch (detail.severity) { diff --git a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts -index 09c9b9b..d2418e0 100644 +index 3094548..fd05b76 100644 --- a/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts +++ b/node_modules/react-compiler-healthcheck/src/checks/reactCompiler.ts -@@ -51,6 +51,9 @@ const COMPILER_OPTIONS: Partial = { - compilationMode: "infer", - panicThreshold: "critical_errors", - logger, +@@ -50,6 +50,9 @@ const COMPILER_OPTIONS: Partial = { + noEmit: true, + compilationMode: 'infer', + panicThreshold: 'critical_errors', + environment: { + enableTreatRefLikeIdentifiersAsRefs: true, + }, + logger, }; - function isActionableDiagnostic(detail: CompilerErrorDetailOptions) { diff --git a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch similarity index 88% rename from patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch rename to patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch index a3de7a365889..246351351195 100644 --- a/patches/react-compiler-healthcheck+0.0.0-experimental-b130d5f-20240625+003+json.patch +++ b/patches/react-compiler-healthcheck+19.0.0-beta-8a03594-20241020+003+json.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/react-compiler-healthcheck/dist/index.js b/node_modules/react-compiler-healthcheck/dist/index.js -index fa2ab22..93be1fb 100755 +index 17b0f96..e386e34 100755 --- a/node_modules/react-compiler-healthcheck/dist/index.js +++ b/node_modules/react-compiler-healthcheck/dist/index.js -@@ -69157,16 +69157,28 @@ var reactCompilerCheck = { +@@ -56972,16 +56972,28 @@ var reactCompilerCheck = { compile(source, path); } }, @@ -24,7 +24,7 @@ index fa2ab22..93be1fb 100755 + ) + ); + } -+ ++ + if (json) { + const extractFileName = (output) => output.fnLoc.filename; + const successfulFiles = SucessfulCompilation.map(extractFileName); @@ -34,10 +34,10 @@ index fa2ab22..93be1fb 100755 + failure: unsuccessfulFiles, + })); + } - + if (verbose) { for (const compilation of [...SucessfulCompilation, ...ActionableFailures, ...OtherFailures]) { -@@ -69253,10 +69265,17 @@ function main() { +@@ -57068,10 +57080,17 @@ function main() { default: false, alias: 'v', }) @@ -48,14 +48,14 @@ index fa2ab22..93be1fb 100755 + alias: 'j', + }) .parseSync(); - const spinner = ora("Checking").start(); + const spinner = ora('Checking').start(); let src = argv.src; let verbose = argv.verbose; + let json = argv.json; const globOptions = { onlyFiles: true, ignore: [ -@@ -69276,9 +69295,12 @@ function main() { +@@ -57091,9 +57110,11 @@ function main() { libraryCompatCheck.run(source, path); } spinner.stop(); @@ -63,7 +63,6 @@ index fa2ab22..93be1fb 100755 - strictModeCheck.report(); - libraryCompatCheck.report(); + reactCompilerCheck.report(verbose, json); -+ // using json option we only want to get list of files + if (!json) { + strictModeCheck.report(); + libraryCompatCheck.report(); diff --git a/patches/react-native+0.75.2+011+textinput-clear-command.patch b/patches/react-native+0.75.2+011+textinput-clear-command.patch index 773dde04ef44..6723d36d6c6c 100644 --- a/patches/react-native+0.75.2+011+textinput-clear-command.patch +++ b/patches/react-native+0.75.2+011+textinput-clear-command.patch @@ -1,3 +1,51 @@ +diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +index a77e5b4..6c4bbb2 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +@@ -412,6 +412,13 @@ export type NativeProps = $ReadOnly<{| + $ReadOnly<{|target: Int32, text: string|}>, + >, + ++ /** ++ * Invoked when the user performs the clear action. ++ */ ++ onClear?: ?BubblingEventHandler< ++ $ReadOnly<{|target: Int32, eventCount: Int32, text: string|}>, ++ >, ++ + /** + * Callback that is called when a key is pressed. + * This will be called with `{ nativeEvent: { key: keyValue } }` +@@ -655,6 +662,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + }, + }, + directEventTypes: { ++ topClear: { ++ registrationName: 'onClear', ++ }, + topScroll: { + registrationName: 'onScroll', + }, +@@ -693,6 +703,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { + textTransform: true, + returnKeyType: true, + keyboardType: true, ++ onClear: true, + multiline: true, + color: {process: require('../../StyleSheet/processColor').default}, + autoComplete: true, +diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +index 0aa8965..0b14171 100644 +--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js ++++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +@@ -146,6 +146,7 @@ const RCTTextInputViewConfig = { + lineBreakStrategyIOS: true, + smartInsertDelete: true, + ...ConditionallyIgnoredEventHandlers({ ++ onClear: true, + onChange: true, + onSelectionChange: true, + onContentSizeChange: true, diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 0aa8965..3bfe22c 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js diff --git a/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch b/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch new file mode 100644 index 000000000000..27fa6074e6ce --- /dev/null +++ b/patches/react-native+0.75.2+018+android-keyboard-avoiding-view.patch @@ -0,0 +1,26 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +index ed1aba8..0a9284f 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java ++++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +@@ -891,7 +891,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { + sendEvent( + "keyboardDidHide", + createKeyboardEventPayload( +- PixelUtil.toDIPFromPixel(mVisibleViewArea.height()), ++ // Use mLastHeight to account for the translucent status bar, and fall back to getMeasuredHeight() on Bridgeless mode. ++ // Remove this patch once the upstream fix for https://github.com/facebook/react-native/issues/47140 is released. ++ PixelUtil.toDIPFromPixel(mWasMeasured ? mLastHeight : getMeasuredHeight()), + 0, + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + 0)); +@@ -940,7 +942,9 @@ public class ReactRootView extends FrameLayout implements RootView, ReactRoot { + sendEvent( + "keyboardDidHide", + createKeyboardEventPayload( +- PixelUtil.toDIPFromPixel(mVisibleViewArea.height()), ++ // Use mLastHeight to account for the translucent status bar, and fall back to getMeasuredHeight() on Bridgeless mode. ++ // Remove this patch once the upstream fix for https://github.com/facebook/react-native/issues/47140 is released. ++ PixelUtil.toDIPFromPixel(mWasMeasured ? mLastHeight : getMeasuredHeight()), + 0, + PixelUtil.toDIPFromPixel(mVisibleViewArea.width()), + 0)); diff --git a/patches/react-native-modal+13.0.1.patch b/patches/react-native-modal+13.0.1.patch index cc9c8531e3a3..bd65871cf5ac 100644 --- a/patches/react-native-modal+13.0.1.patch +++ b/patches/react-native-modal+13.0.1.patch @@ -11,7 +11,7 @@ index b63bcfc..bd6419e 100644 buildPanResponder: () => void; getAccDistancePerDirection: (gestureState: PanResponderGestureState) => number; diff --git a/node_modules/react-native-modal/dist/modal.js b/node_modules/react-native-modal/dist/modal.js -index 80f4e75..5a58eae 100644 +index 80f4e75..46277ea 100644 --- a/node_modules/react-native-modal/dist/modal.js +++ b/node_modules/react-native-modal/dist/modal.js @@ -75,6 +75,13 @@ export class ReactNativeModal extends React.Component { @@ -28,7 +28,18 @@ index 80f4e75..5a58eae 100644 this.shouldPropagateSwipe = (evt, gestureState) => { return typeof this.props.propagateSwipe === 'function' ? this.props.propagateSwipe(evt, gestureState) -@@ -453,10 +460,18 @@ export class ReactNativeModal extends React.Component { +@@ -383,7 +390,9 @@ export class ReactNativeModal extends React.Component { + this.setState({ + isVisible: false, + }, () => { +- this.props.onModalHide(); ++ if (Platform.OS !== 'ios') { ++ this.props.onModalHide(); ++ } + }); + }); + } +@@ -453,10 +462,18 @@ export class ReactNativeModal extends React.Component { if (this.state.isVisible) { this.open(); } @@ -48,7 +59,7 @@ index 80f4e75..5a58eae 100644 if (this.didUpdateDimensionsEmitter) { this.didUpdateDimensionsEmitter.remove(); } -@@ -464,6 +479,9 @@ export class ReactNativeModal extends React.Component { +@@ -464,6 +481,9 @@ export class ReactNativeModal extends React.Component { InteractionManager.clearInteractionHandle(this.interactionHandle); this.interactionHandle = null; } @@ -58,9 +69,21 @@ index 80f4e75..5a58eae 100644 } componentDidUpdate(prevProps) { // If the animations have been changed then rebuild them to make sure we're -@@ -525,7 +543,7 @@ export class ReactNativeModal extends React.Component { +@@ -490,7 +510,7 @@ export class ReactNativeModal extends React.Component { + } + render() { + /* eslint-disable @typescript-eslint/no-unused-vars */ +- const { animationIn, animationInTiming, animationOut, animationOutTiming, avoidKeyboard, coverScreen, hasBackdrop, backdropColor, backdropOpacity, backdropTransitionInTiming, backdropTransitionOutTiming, customBackdrop, children, isVisible, onModalShow, onBackButtonPress, useNativeDriver, propagateSwipe, style, ...otherProps } = this.props; ++ const { animationIn, animationInTiming, animationOut, animationOutTiming, avoidKeyboard, coverScreen, hasBackdrop, backdropColor, backdropOpacity, backdropTransitionInTiming, backdropTransitionOutTiming, customBackdrop, children, isVisible, onModalShow, onBackButtonPress, useNativeDriver, propagateSwipe, style, onDismiss, ...otherProps } = this.props; + const { testID, ...containerProps } = otherProps; + const computedStyle = [ + { margin: this.getDeviceWidth() * 0.05, transform: [{ translateY: 0 }] }, +@@ -523,9 +543,9 @@ export class ReactNativeModal extends React.Component { + this.makeBackdrop(), + containerView)); } - return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), +- return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress }, otherProps), ++ return (React.createElement(Modal, Object.assign({ transparent: true, animationType: 'none', visible: this.state.isVisible, onRequestClose: onBackButtonPress, onDismiss: () => {onDismiss();if (Platform.OS === 'ios'){this.props.onModalHide();}} }, otherProps), this.makeBackdrop(), - avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView))); + avoidKeyboard ? (React.createElement(KeyboardAvoidingView, { behavior: 'padding', pointerEvents: "box-none", style: computedStyle.concat([{ margin: 0 }]) }, containerView)) : (containerView))); diff --git a/patches/react-native-pager-view+6.4.1.patch b/patches/react-native-pager-view+6.4.1.patch new file mode 100644 index 000000000000..64b2b580ecd3 --- /dev/null +++ b/patches/react-native-pager-view+6.4.1.patch @@ -0,0 +1,73 @@ +--- a/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm ++++ b/node_modules/react-native-pager-view/ios/Fabric/RNCPagerViewComponentView.mm +@@ -195,13 +195,10 @@ -(void)scrollViewDidScroll:(UIScrollView *)scrollView { + + strongEventEmitter.onPageScroll(RNCViewPagerEventEmitter::OnPageScroll{.position = static_cast(position), .offset = offset}); + +- //This is temporary workaround to allow animations based on onPageScroll event +- //until Fabric implements proper NativeAnimationDriver +- RCTBridge *bridge = [RCTBridge currentBridge]; +- +- if (bridge) { +- [bridge.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(offset)]]; +- } ++ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(offset)], @"event", nil]; ++ [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED" ++ object:nil ++ userInfo:userInfo]; + } + + #pragma mark - Internal methods +diff --git a/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm b/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm +index 7608645..84f6f60 100644 +--- a/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm ++++ b/node_modules/react-native-pager-view/ios/LEGACY/Fabric/LEGACY_RNCPagerViewComponentView.mm +@@ -363,14 +363,10 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + int eventPosition = (int) position; + strongEventEmitter.onPageScroll(LEGACY_RNCViewPagerEventEmitter::OnPageScroll{.position = static_cast(eventPosition), .offset = interpolatedOffset}); + +- //This is temporary workaround to allow animations based on onPageScroll event +- //until Fabric implements proper NativeAnimationDriver +- RCTBridge *bridge = [RCTBridge currentBridge]; +- +- if (bridge) { +- [bridge.eventDispatcher sendEvent:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(interpolatedOffset)]]; +- } +- ++ NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:[[RCTOnPageScrollEvent alloc] initWithReactTag:[NSNumber numberWithInt:self.tag] position:@(position) offset:@(interpolatedOffset)], @"event", nil]; ++ [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED" ++ object:nil ++ userInfo:userInfo]; + } + + +diff --git a/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m b/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m +index 5f6c535..fd6c2a1 100644 +--- a/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m ++++ b/node_modules/react-native-pager-view/ios/LEGACY/LEGACY_RNCPagerView.m +@@ -1,5 +1,5 @@ + #import "LEGACY_RNCPagerView.h" +-#import "React/RCTLog.h" ++#import + #import + + #import "UIViewController+CreateExtension.h" +diff --git a/node_modules/react-native-pager-view/ios/RNCPagerView.m b/node_modules/react-native-pager-view/ios/RNCPagerView.m +index 584aada..978496f 100644 +--- a/node_modules/react-native-pager-view/ios/RNCPagerView.m ++++ b/node_modules/react-native-pager-view/ios/RNCPagerView.m +@@ -1,12 +1,12 @@ + + #import "RNCPagerView.h" +-#import "React/RCTLog.h" ++#import + #import + + #import "UIViewController+CreateExtension.h" + #import "RCTOnPageScrollEvent.h" + #import "RCTOnPageScrollStateChanged.h" +-#import "React/RCTUIManagerObserverCoordinator.h" ++#import + #import "RCTOnPageSelected.h" + #import + diff --git a/patches/react-native-performance+5.1.0+001+bridgeless.patch b/patches/react-native-performance+5.1.0+001+bridgeless.patch new file mode 100644 index 000000000000..7aed8cf57487 --- /dev/null +++ b/patches/react-native-performance+5.1.0+001+bridgeless.patch @@ -0,0 +1,30 @@ +diff --git a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java +index 2fa7d5d..10e1ba6 100644 +--- a/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java ++++ b/node_modules/react-native-performance/android/src/main/java/com/oblador/performance/PerformanceModule.java +@@ -17,7 +17,7 @@ import java.util.Queue; + import java.util.concurrent.ConcurrentLinkedQueue; + + // Should extend NativeRNPerformanceManagerSpec when codegen for old architecture is solved +-public class PerformanceModule extends ReactContextBaseJavaModule implements TurboModule, RNPerformance.MarkerListener { ++public class PerformanceModule extends NativeRNPerformanceManagerSpec implements RNPerformance.MarkerListener { + public static final String PERFORMANCE_MODULE = "RNPerformanceManager"; + public static final String BRIDGE_SETUP_START = "bridgeSetupStart"; + +@@ -118,6 +118,16 @@ public class PerformanceModule extends ReactContextBaseJavaModule implements Tur + return PERFORMANCE_MODULE; + } + ++ @Override ++ public void addListener(String eventName) { ++ // needed for spec ++ } ++ ++ @Override ++ public void removeListeners(double count) { ++ // needed for spec ++ } ++ + private void emitNativeStartupTime() { + safelyEmitMark(new PerformanceMark("nativeLaunchStart", StartTimeProvider.getStartTime())); + safelyEmitMark(new PerformanceMark("nativeLaunchEnd", StartTimeProvider.getEndTime())); diff --git a/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch b/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch new file mode 100644 index 000000000000..8f8a13d684e5 --- /dev/null +++ b/patches/react-native-quick-sqlite+8.1.0+001+bridgeless.patch @@ -0,0 +1,41 @@ +diff --git a/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm b/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm +index 519f31a..308f746 100644 +--- a/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm ++++ b/node_modules/react-native-quick-sqlite/ios/QuickSQLite.mm +@@ -12,12 +12,12 @@ @implementation QuickSQLite + + RCT_EXPORT_MODULE(QuickSQLite) + ++@synthesize bridge = _bridge; + + RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { + NSLog(@"Installing QuickSQLite module..."); + +- RCTBridge *bridge = [RCTBridge currentBridge]; +- RCTCxxBridge *cxxBridge = (RCTCxxBridge *)bridge; ++ RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge; + if (cxxBridge == nil) { + return @false; + } +@@ -29,7 +29,7 @@ @implementation QuickSQLite + return @false; + } + auto &runtime = *jsiRuntime; +- auto callInvoker = bridge.jsCallInvoker; ++ auto callInvoker = cxxBridge.jsCallInvoker; + + // Get appGroupID value from Info.plist using key "AppGroup" + NSString *appGroupID = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"ReactNativeQuickSQLite_AppGroup"]; +diff --git a/node_modules/react-native-quick-sqlite/src/index.ts b/node_modules/react-native-quick-sqlite/src/index.ts +index b3e7fc7..7d8930a 100644 +--- a/node_modules/react-native-quick-sqlite/src/index.ts ++++ b/node_modules/react-native-quick-sqlite/src/index.ts +@@ -15,7 +15,7 @@ if (global.__QuickSQLiteProxy == null) { + } + + // Check if we are running on-device (JSI) +- if (global.nativeCallSyncHook == null || QuickSQLiteModule.install == null) { ++ if ((!global.nativeCallSyncHook && !global.RN$Bridgeless) || QuickSQLiteModule.install == null) { + throw new Error( + 'Failed to install react-native-quick-sqlite: React Native is not running on-device. QuickSQLite can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.' + ); diff --git a/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch b/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch index 4e0961ec536a..7c585ddf9f27 100644 --- a/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch +++ b/patches/react-native-vision-camera+4.0.0-beta.13+001+rn75-compatibility.patch @@ -729,10 +729,10 @@ index 25e1f55..33b9dd3 100644 + } } diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt -index f2b284c..e348e5c 100644 +index f2b284c..4bb2ebc 100644 --- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt -@@ -4,7 +4,10 @@ import com.facebook.react.bridge.ReadableMap +@@ -4,8 +4,18 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager @@ -740,10 +740,18 @@ index f2b284c..e348e5c 100644 import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.CameraViewManagerDelegate +import com.facebook.react.viewmanagers.CameraViewManagerInterface ++import com.mrousavy.camera.types.CameraCodeScannedEvent import com.mrousavy.camera.types.CameraDeviceFormat ++import com.mrousavy.camera.types.CameraErrorEvent ++import com.mrousavy.camera.types.CameraInitializedEvent ++import com.mrousavy.camera.types.CameraShutterEvent ++import com.mrousavy.camera.types.CameraStartedEvent ++import com.mrousavy.camera.types.CameraStoppedEvent ++import com.mrousavy.camera.types.CameraViewReadyEvent import com.mrousavy.camera.types.CodeScannerOptions import com.mrousavy.camera.types.Orientation -@@ -16,10 +19,19 @@ import com.mrousavy.camera.types.Torch + import com.mrousavy.camera.types.PixelFormat +@@ -16,10 +26,19 @@ import com.mrousavy.camera.types.Torch import com.mrousavy.camera.types.VideoStabilizationMode @Suppress("unused") @@ -764,7 +772,28 @@ index f2b284c..e348e5c 100644 public override fun createViewInstance(context: ThemedReactContext): CameraView = CameraView(context) override fun onAfterUpdateTransaction(view: CameraView) { -@@ -46,37 +58,37 @@ class CameraViewManager : ViewGroupManager() { +@@ -29,13 +48,13 @@ class CameraViewManager : ViewGroupManager() { + + override fun getExportedCustomDirectEventTypeConstants(): MutableMap? = + MapBuilder.builder() +- .put("cameraViewReady", MapBuilder.of("registrationName", "onViewReady")) +- .put("cameraInitialized", MapBuilder.of("registrationName", "onInitialized")) +- .put("cameraStarted", MapBuilder.of("registrationName", "onStarted")) +- .put("cameraStopped", MapBuilder.of("registrationName", "onStopped")) +- .put("cameraShutter", MapBuilder.of("registrationName", "onShutter")) +- .put("cameraError", MapBuilder.of("registrationName", "onError")) +- .put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned")) ++ .put(CameraViewReadyEvent.EVENT_NAME, MapBuilder.of("registrationName", "onViewReady")) ++ .put(CameraInitializedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onInitialized")) ++ .put(CameraStartedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onStarted")) ++ .put(CameraStoppedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onStopped")) ++ .put(CameraShutterEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShutter")) ++ .put(CameraErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onError")) ++ .put(CameraCodeScannedEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCodeScanned")) + .build() + + override fun getName(): String = TAG +@@ -46,37 +65,37 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "cameraId") @@ -809,7 +838,7 @@ index f2b284c..e348e5c 100644 if (pixelFormat != null) { val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) view.pixelFormat = newPixelFormat -@@ -86,27 +98,27 @@ class CameraViewManager : ViewGroupManager() { +@@ -86,27 +105,27 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "enableDepthData") @@ -842,7 +871,7 @@ index f2b284c..e348e5c 100644 if (videoStabilizationMode != null) { val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) view.videoStabilizationMode = newMode -@@ -116,12 +128,12 @@ class CameraViewManager : ViewGroupManager() { +@@ -116,12 +135,12 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "enablePortraitEffectsMatteDelivery") @@ -857,7 +886,7 @@ index f2b284c..e348e5c 100644 if (format != null) { val newFormat = CameraDeviceFormat.fromJSValue(format) view.format = newFormat -@@ -131,7 +143,7 @@ class CameraViewManager : ViewGroupManager() { +@@ -131,7 +150,7 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "resizeMode") @@ -866,7 +895,7 @@ index f2b284c..e348e5c 100644 if (resizeMode != null) { val newMode = ResizeMode.fromUnionValue(resizeMode) view.resizeMode = newMode -@@ -141,7 +153,7 @@ class CameraViewManager : ViewGroupManager() { +@@ -141,7 +160,7 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "androidPreviewViewType") @@ -875,7 +904,7 @@ index f2b284c..e348e5c 100644 if (androidPreviewViewType != null) { val newMode = PreviewViewType.fromUnionValue(androidPreviewViewType) view.androidPreviewViewType = newMode -@@ -154,17 +166,17 @@ class CameraViewManager : ViewGroupManager() { +@@ -154,17 +173,17 @@ class CameraViewManager : ViewGroupManager() { // We're treating -1 as "null" here, because when I make the fps parameter // of type "Int?" the react bridge throws an error. @ReactProp(name = "fps", defaultInt = -1) @@ -896,7 +925,7 @@ index f2b284c..e348e5c 100644 if (photoQualityBalance != null) { val newMode = QualityBalance.fromUnionValue(photoQualityBalance) view.photoQualityBalance = newMode -@@ -174,22 +186,22 @@ class CameraViewManager : ViewGroupManager() { +@@ -174,22 +193,22 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "videoHdr") @@ -923,7 +952,7 @@ index f2b284c..e348e5c 100644 if (torch != null) { val newMode = Torch.fromUnionValue(torch) view.torch = newMode -@@ -199,17 +211,17 @@ class CameraViewManager : ViewGroupManager() { +@@ -199,17 +218,17 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "zoom") @@ -944,7 +973,7 @@ index f2b284c..e348e5c 100644 if (orientation != null) { val newMode = Orientation.fromUnionValue(orientation) view.orientation = newMode -@@ -219,7 +231,7 @@ class CameraViewManager : ViewGroupManager() { +@@ -219,7 +238,7 @@ class CameraViewManager : ViewGroupManager() { } @ReactProp(name = "codeScannerOptions") @@ -953,7 +982,7 @@ index f2b284c..e348e5c 100644 if (codeScannerOptions != null) { val newCodeScannerOptions = CodeScannerOptions.fromJSValue(codeScannerOptions) view.codeScannerOptions = newCodeScannerOptions -@@ -227,4 +239,8 @@ class CameraViewManager : ViewGroupManager() { +@@ -227,4 +246,8 @@ class CameraViewManager : ViewGroupManager() { view.codeScannerOptions = null } } @@ -981,6 +1010,79 @@ index b9d3f67..cb70963 100644 @Suppress("KotlinJniMissingFunction") // we use fbjni. class VisionCameraProxy(private val reactContext: ReactApplicationContext) { companion object { +diff --git a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt +index 1ed0355..b8ff7cf 100644 +--- a/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt ++++ b/node_modules/react-native-vision-camera/android/src/main/java/com/mrousavy/camera/types/Events.kt +@@ -3,39 +3,61 @@ package com.mrousavy.camera.types + import com.facebook.react.bridge.Arguments + import com.facebook.react.bridge.WritableMap + import com.facebook.react.uimanager.events.Event ++import com.mrousavy.camera.types.CameraInitializedEvent.Companion.EVENT_NAME + + class CameraInitializedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraInitialized" ++ override fun getEventName() = EVENT_NAME + override fun getEventData(): WritableMap = Arguments.createMap() ++ companion object { ++ const val EVENT_NAME = "topInitialized" ++ } + } + + class CameraStartedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraStarted" ++ override fun getEventName() = EVENT_NAME + override fun getEventData(): WritableMap = Arguments.createMap() ++ companion object { ++ const val EVENT_NAME = "topStarted" ++ } + } + + class CameraStoppedEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraStopped" ++ override fun getEventName() = EVENT_NAME + override fun getEventData(): WritableMap = Arguments.createMap() ++ companion object { ++ const val EVENT_NAME = "topStopped" ++ } + } + + class CameraShutterEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraShutter" ++ override fun getEventName() = EVENT_NAME + override fun getEventData() = data ++ companion object { ++ const val EVENT_NAME = "topShutter" ++ } + } + + class CameraErrorEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraError" ++ override fun getEventName() = EVENT_NAME + override fun getEventData() = data ++ companion object { ++ const val EVENT_NAME = "topError" ++ } + } + + class CameraViewReadyEvent(surfaceId: Int, viewId: Int) : Event(surfaceId, viewId) { +- override fun getEventName() = "cameraViewReady" ++ override fun getEventName() = EVENT_NAME + override fun getEventData(): WritableMap = Arguments.createMap() ++ companion object { ++ const val EVENT_NAME = "topViewReady" ++ } + } + + class CameraCodeScannedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : + Event(surfaceId, viewId) { +- override fun getEventName() = "cameraCodeScanned" ++ override fun getEventName() = EVENT_NAME + override fun getEventData() = data ++ companion object { ++ const val EVENT_NAME = "topCodeScanned" ++ } + } diff --git a/node_modules/react-native-vision-camera/ios/.swift-version b/node_modules/react-native-vision-camera/ios/.swift-version new file mode 100644 index 0000000..ef425ca diff --git a/src/CONST.ts b/src/CONST.ts index 440f942e1244..8e240ac43ca1 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -297,6 +297,9 @@ const CONST = { // Regex to get link in href prop inside of component REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi, + // Regex to read violation value from string given by backend + VIOLATION_LIMIT_REGEX: /[^0-9]+/g, + MERCHANT_NAME_MAX_LENGTH: 255, MASKED_PAN_PREFIX: 'XXXXXXXXXXXX', @@ -506,7 +509,6 @@ const CONST = { WORKSPACE_RULES: 'workspaceRules', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', - NEW_DOT_QBD: 'quickbooksDesktopOnNewDot', }, BUTTON_STATES: { DEFAULT: 'default', @@ -1574,6 +1576,7 @@ const CONST = { TRACKING_CATEGORY_OPTIONS: { DEFAULT: 'DEFAULT', TAG: 'TAG', + REPORT_FIELD: 'REPORT_FIELD', }, }, @@ -1626,6 +1629,12 @@ const CONST = { JOURNAL_ENTRY: 'journal_entry', }, + QUICKBOOKS_NON_REIMBURSABLE_ACCOUNT_TYPE: { + CREDIT_CARD: 'credit_card', + DEBIT_CARD: 'debit_card', + VENDOR_BILL: 'bill', + }, + QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE: { VENDOR_BILL: 'VENDOR_BILL', CHECK: 'CHECK', @@ -2599,6 +2608,7 @@ const CONST = { MONTHLY: 'monthly', }, CARD_TITLE_INPUT_LIMIT: 255, + MANAGE_EXPENSIFY_CARDS_ARTICLE_LINK: 'https://help.expensify.com/articles/new-expensify/expensify-card/Manage-Expensify-Cards', }, COMPANY_CARDS: { CONNECTION_ERROR: 'connectionError', @@ -2635,6 +2645,7 @@ const CONST = { }, BANK_CONNECTIONS: { WELLS_FARGO: 'wellsfargo', + BANK_OF_AMERICA: 'bankofamerica', CHASE: 'chase', BREX: 'brex', CAPITAL_ONE: 'capitalone', @@ -3310,6 +3321,63 @@ const CONST = { ZW: 'Zimbabwe', }, + ALL_EUROPEAN_COUNTRIES: { + AL: 'Albania', + AD: 'Andorra', + AT: 'Austria', + BY: 'Belarus', + BE: 'Belgium', + BA: 'Bosnia & Herzegovina', + BG: 'Bulgaria', + HR: 'Croatia', + CY: 'Cyprus', + CZ: 'Czech Republic', + DK: 'Denmark', + EE: 'Estonia', + FO: 'Faroe Islands', + FI: 'Finland', + FR: 'France', + GE: 'Georgia', + DE: 'Germany', + GI: 'Gibraltar', + GR: 'Greece', + GL: 'Greenland', + HU: 'Hungary', + IS: 'Iceland', + IE: 'Ireland', + IM: 'Isle of Man', + IT: 'Italy', + JE: 'Jersey', + XK: 'Kosovo', + LV: 'Latvia', + LI: 'Liechtenstein', + LT: 'Lithuania', + LU: 'Luxembourg', + MT: 'Malta', + MD: 'Moldova', + MC: 'Monaco', + ME: 'Montenegro', + NL: 'Netherlands', + MK: 'North Macedonia', + NO: 'Norway', + PL: 'Poland', + PT: 'Portugal', + RO: 'Romania', + RU: 'Russia', + SM: 'San Marino', + RS: 'Serbia', + SK: 'Slovakia', + SI: 'Slovenia', + ES: 'Spain', + SJ: 'Svalbard & Jan Mayen', + SE: 'Sweden', + CH: 'Switzerland', + TR: 'Turkey', + UA: 'Ukraine', + GB: 'United Kingdom', + VA: 'Vatican City', + }, + // Sources: https://github.com/Expensify/App/issues/14958#issuecomment-1442138427 // https://github.com/Expensify/App/issues/14958#issuecomment-1456026810 COUNTRY_ZIP_REGEX_DATA: { @@ -4825,10 +4893,11 @@ const CONST = { '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Click the green *+* button.\n' + - '2. Choose *Split expense*.\n' + - '3. Scan a receipt or enter an amount.\n' + - '4. Add your friend(s) to the request.\n' + + '1. Hit the green *+* button.\n' + + '2. Choose *Start chat*.\n' + + '3. Enter any email, SMS, or name of who you want to split with.\n' + + '4. From within the chat, hit the *+* button on the message bar, and hit *Split expense*.\n' + + '5. Create the expense by selecting Manual, Scan or Distance.\n' + '\n' + 'Feel free to add more details if you want, or just send it off. Let’s get you paid back!', }, @@ -5944,6 +6013,7 @@ const CONST = { HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors', HAS_LOGIN_LIST_INFO: 'hasLoginListInfo', HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo', + HAS_PHONE_NUMBER_ERROR: 'hasPhoneNumberError', }, DEBUG: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d083a46d7760..427e05052ae3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,6 +3,7 @@ import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; import type * as FormTypes from './types/form'; import type * as OnyxTypes from './types/onyx'; +import type {Attendee} from './types/onyx/IOU'; import type Onboarding from './types/onyx/Onboarding'; import type AssertTypesEqual from './types/utils/AssertTypesEqual'; import type DeepValueOf from './types/utils/DeepValueOf'; @@ -112,6 +113,9 @@ const ONYXKEYS = { /** Boolean flag only true when first set */ NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** This NVP contains list of at most 5 recent attendees */ + NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees', + /** This NVP contains information about whether the onboarding flow was completed or not */ NVP_ONBOARDING: 'nvp_onboarding', @@ -905,6 +909,7 @@ type OnyxValuesMapping = { // The value of this nvp is a string representation of the date when the block expires, or an empty string if the user is not blocked [ONYXKEYS.NVP_BLOCKED_FROM_CHAT]: string; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; + [ONYXKEYS.NVP_RECENT_ATTENDEES]: Attendee[]; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; [ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 23ef269d8cfb..2e895537eaac 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -208,6 +208,7 @@ const ROUTES = { }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', + SETTINGS_PHONE_NUMBER: 'settings/profile/phone', SETTINGS_ADDRESS: 'settings/profile/address', SETTINGS_ADDRESS_COUNTRY: { route: 'settings/profile/address/country', @@ -447,6 +448,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, + MONEY_REQUEST_ATTENDEE: { + route: ':action/:iouType/attendees/:transactionID/:reportID', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo), + }, SETTINGS_TAGS_ROOT: { route: 'settings/:policyID/tags', getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo), @@ -1445,14 +1451,26 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/classes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/classes` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/classes/displayed-as', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/classes/displayed-as` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/customers', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/customers` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/customers/displayed-as', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/customers/displayed-as` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/locations', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/locations` as const, }, + POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: { + route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/locations/displayed-as', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/locations/displayed-as` as const, + }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_TAXES: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import/taxes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3a33975b2bbe..feded7c81a47 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -84,6 +84,7 @@ const SCREENS = { TIMEZONE_SELECT: 'Settings_Timezone_Select', LEGAL_NAME: 'Settings_LegalName', DATE_OF_BIRTH: 'Settings_DateOfBirth', + PHONE_NUMBER: 'Settings_PhoneNumber', ADDRESS: 'Settings_Address', ADDRESS_COUNTRY: 'Settings_Address_Country', ADDRESS_STATE: 'Settings_Address_State', @@ -219,6 +220,7 @@ const SCREENS = { EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', RECEIPT: 'Money_Request_Receipt', STATE_SELECTOR: 'Money_Request_State_Selector', + STEP_ATTENDEES: 'Money_Request_Attendee', }, TRANSACTION_DUPLICATE: { @@ -331,6 +333,9 @@ const SCREENS = { QUICKBOOKS_ONLINE_ADVANCED: 'Policy_Accounting_Quickbooks_Online_Advanced', QUICKBOOKS_ONLINE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Account_Selector', QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector', + QUICKBOOKS_ONLINE_CLASSES_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Classes_Displayed_As', + QUICKBOOKS_ONLINE_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Customers_Displayed_As', + QUICKBOOKS_ONLINE_LOCATIONS_DISPLAYED_AS: 'Policy_Accounting_Quickbooks_Online_Import_Locations_Displayed_As', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Account_Select', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT_COMPANY_CARD_SELECT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense_Select', QUICKBOOKS_DESKTOP_COMPANY_CARD_EXPENSE_ACCOUNT: 'Workspace_Accounting_Quickbooks_Desktop_Export_Company_Card_Expense', diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 443a553d4689..ae74a11c7e9d 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -22,7 +22,7 @@ type BaseAnchorForAttachmentsOnlyProps = AnchorForAttachmentsOnlyProps & { onPressOut?: () => void; }; -function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut}: BaseAnchorForAttachmentsOnlyProps) { +function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onPressIn, onPressOut, isDeleted}: BaseAnchorForAttachmentsOnlyProps) { const sourceURLWithAuth = addEncryptedAuthTokenToURL(source); const sourceID = (source.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; @@ -63,6 +63,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP shouldShowDownloadIcon={!!sourceID && !isOffline} shouldShowLoadingSpinnerIcon={isDownloading} isUsedAsChatAttachment + isDeleted={!!isDeleted} isUploading={!sourceID} /> diff --git a/src/components/AnchorForAttachmentsOnly/types.ts b/src/components/AnchorForAttachmentsOnly/types.ts index a5186d8c0d90..67a5bb532c27 100644 --- a/src/components/AnchorForAttachmentsOnly/types.ts +++ b/src/components/AnchorForAttachmentsOnly/types.ts @@ -9,6 +9,9 @@ type AnchorForAttachmentsOnlyProps = { /** Any additional styles to apply */ style?: StyleProp; + + /** Whether the attachment is deleted */ + isDeleted?: boolean; }; export default AnchorForAttachmentsOnlyProps; diff --git a/src/components/AttachmentDeletedIndicator.tsx b/src/components/AttachmentDeletedIndicator.tsx new file mode 100644 index 000000000000..06e700c2fd73 --- /dev/null +++ b/src/components/AttachmentDeletedIndicator.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; + +type AttachmentDeletedIndicatorProps = { + /** Additional styles for container */ + containerStyles?: StyleProp; +}; + +function AttachmentDeletedIndicator({containerStyles}: AttachmentDeletedIndicatorProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + if (!isOffline) { + return null; + } + + return ( + <> + + + + + + ); +} + +AttachmentDeletedIndicator.displayName = 'AttachmentDeletedIndicator'; + +export default AttachmentDeletedIndicator; diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 4a357178d5f3..85824f81e313 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -421,6 +421,7 @@ function AttachmentPicker({ }} isVisible={isVisible} anchorRef={popoverRef} + // eslint-disable-next-line react-compiler/react-compiler onModalHide={onModalHide.current} > @@ -436,6 +437,7 @@ function AttachmentPicker({ ))} + {/* eslint-disable-next-line react-compiler/react-compiler */} {renderChildren()} ); diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index c4979f544080..f3c880fcb835 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -98,6 +98,7 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a }} accept={acceptedFileTypes ? getAcceptableFileTypesFromAList(acceptedFileTypes) : getAcceptableFileTypes(type)} /> + {/* eslint-disable-next-line react-compiler/react-compiler */} {children({ openPicker: ({onPicked: newOnPicked, onCanceled: newOnCanceled = () => {}}) => { onPicked.current = newOnPicked; diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index a1408aaf400e..d9c4f7e93fbe 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -255,6 +255,7 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi scrollTo(scrollRef, newIndex * cellWidth, 0, true); }) + // eslint-disable-next-line react-compiler/react-compiler .withRef(pagerRef as MutableRefObject), [attachments.length, canUseTouchScreen, cellWidth, page, scale, scrollRef], ); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx index 1e3cded92bd5..c6e7984b793f 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx @@ -32,7 +32,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { const Pan = Gesture.Pan() .manualActivation(true) .onTouchesMove((evt) => { - if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled) { + if (offsetX.value !== 0 && offsetY.value !== 0 && isScrollEnabled && scale.value === 1) { const translateX = Math.abs((evt.allTouches.at(0)?.absoluteX ?? 0) - offsetX.value); const translateY = Math.abs((evt.allTouches.at(0)?.absoluteY ?? 0) - offsetY.value); const allowEnablingScroll = !isPanGestureActive.value || isScrollEnabled.value; @@ -40,7 +40,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { // if the value of X is greater than Y and the pdf is not zoomed in, // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. - if (translateX > translateY && translateX > SCROLL_THRESHOLD && scale.value === 1 && allowEnablingScroll) { + if (translateX > translateY && translateX > SCROLL_THRESHOLD && allowEnablingScroll) { // eslint-disable-next-line react-compiler/react-compiler isScrollEnabled.value = true; } else if (translateY > SCROLL_THRESHOLD) { @@ -57,7 +57,7 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) { if (!isScrollEnabled) { return; } - isScrollEnabled.value = true; + isScrollEnabled.value = scale.value === 1; }); const Content = useMemo( diff --git a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx index e6ac9f9f21c7..23e13833df64 100644 --- a/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/DefaultAttachmentView/index.tsx @@ -25,11 +25,14 @@ type DefaultAttachmentViewProps = { icon?: IconAsset; + /** Whether the attachment is deleted */ + isDeleted?: boolean; + /** Flag indicating if the attachment is being uploaded. */ isUploading?: boolean; }; -function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading}: DefaultAttachmentViewProps) { +function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon, isUploading, isDeleted}: DefaultAttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -43,7 +46,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa /> - {fileName} + {fileName} {!shouldShowLoadingSpinnerIcon && shouldShowDownloadIcon && ( diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 080e0ec589ec..0af1a86992e7 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -72,6 +72,9 @@ type AttachmentViewProps = Attachment & { /* Flag indicating whether the attachment has been uploaded. */ isUploaded?: boolean; + /** Whether the attachment is deleted */ + isDeleted?: boolean; + /** Flag indicating if the attachment is being uploaded. */ isUploading?: boolean; }; @@ -98,14 +101,14 @@ function AttachmentView({ duration, isUsedAsChatAttachment, isUploaded = true, + isDeleted, isUploading = false, }: AttachmentViewProps) { + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const {translate} = useLocalize(); const {updateCurrentlyPlayingURL} = usePlaybackContext(); const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); - const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -292,6 +295,7 @@ function AttachmentView({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon || isUploading} containerStyles={containerStyles} + isDeleted={isDeleted} isUploading={isUploading} /> ); diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 1a606b35f6d2..dca0d08d11d5 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -336,6 +336,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose } const newSliderValue = clamp(locationX, [0, sliderContainerSize]); const newScale = newScaleValue(newSliderValue, sliderContainerSize); + // eslint-disable-next-line react-compiler/react-compiler translateSlider.value = newSliderValue; const differential = newScale / scale.value; scale.value = newScale; diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index e1d7beb850d0..c44054c15445 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -53,6 +53,7 @@ function ButtonWithDropdownMenu({ const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null); const {windowWidth, windowHeight} = useWindowDimensions(); const dropdownAnchor = useRef(null); + // eslint-disable-next-line react-compiler/react-compiler const dropdownButtonRef = isSplitButton ? buttonRef : mergeRefs(buttonRef, dropdownAnchor); const selectedItem = options.at(selectedItemIndex) ?? options.at(0); const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); @@ -200,6 +201,7 @@ function ButtonWithDropdownMenu({ onItemSelected={() => setIsMenuVisible(false)} anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverButtonDropdownMenuOffset(windowWidth) : popoverAnchorPosition} shouldShowSelectedItemCheck={shouldShowSelectedItemCheck} + // eslint-disable-next-line react-compiler/react-compiler anchorRef={nullCheckRef(dropdownAnchor)} withoutOverlay anchorAlignment={anchorAlignment} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index da6b3d93433d..766c0df950b4 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -25,6 +25,7 @@ type DropdownOption = { iconWidth?: number; iconHeight?: number; iconDescription?: string; + additionalIconStyles?: StyleProp; onSelected?: () => void; disabled?: boolean; iconFill?: string; diff --git a/src/components/ConnectBankAccountButton.tsx b/src/components/ConnectBankAccountButton.tsx deleted file mode 100644 index ee6ad04d727e..000000000000 --- a/src/components/ConnectBankAccountButton.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; -import {View} from 'react-native'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; -import Button from './Button'; -import * as Expensicons from './Icon/Expensicons'; -import Text from './Text'; - -type ConnectBankAccountButtonProps = { - /** PolicyID for navigating to bank account route of that policy */ - policyID: string; - - /** Button styles, also applied for offline message wrapper */ - style?: StyleProp; -}; - -function ConnectBankAccountButton({style, policyID}: ConnectBankAccountButtonProps) { - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - const activeRoute = Navigation.getActiveRouteWithoutParams(); - - return isOffline ? ( - - {`${translate('common.youAppearToBeOffline')} ${translate('common.thisFeatureRequiresInternet')}`} - - ) : ( -