diff --git a/.eslintignore b/.eslintignore index 162cc816ea80..26ecb1ae7cc7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -12,4 +12,5 @@ docs/assets/** web/gtm.js **/.expo/** src/libs/SearchParser/searchParser.js +src/libs/SearchParser/autocompleteParser.js help/_scripts/** 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/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/deployNewHelp.yml b/.github/workflows/deployNewHelp.yml index 45a4ab7c3620..2d2f551482d2 100644 --- a/.github/workflows/deployNewHelp.yml +++ b/.github/workflows/deployNewHelp.yml @@ -55,7 +55,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '20.15.1' + node-version: '20.18.0' # Wil install the _help/package.js - name: Install Node.js Dependencies diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index d8e706d467ba..b48c7b2175eb 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -24,33 +24,21 @@ jobs: runs-on: ubuntu-latest name: Find the baseline and delta refs, and check for an existing build artifact for that commit outputs: - BASELINE_ARTIFACT_FOUND: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND }} - BASELINE_ARTIFACT_WORKFLOW_ID: ${{ steps.checkForExistingArtifact.outputs.ARTIFACT_WORKFLOW_ID }} - BASELINE_VERSION: ${{ steps.getMostRecentRelease.outputs.VERSION }} + BASELINE_REF: ${{ steps.getBaselineRef.outputs.BASELINE_REF }} DELTA_REF: ${{ steps.getDeltaRef.outputs.DELTA_REF }} IS_PR_MERGED: ${{ steps.getPullRequestDetails.outputs.IS_MERGED }} steps: - uses: actions/checkout@v4 with: - # The OS_BOTIFY_COMMIT_TOKEN is a personal access token tied to osbotify (we need a PAT to access the artifact API) - token: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - - name: Get most recent release version - id: getMostRecentRelease - run: echo "VERSION=$(gh release list --limit 1 | awk '{ print $1 }')" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Check if there's an existing artifact for this baseline - id: checkForExistingArtifact - uses: ./.github/actions/javascript/getArtifactInfo - with: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - ARTIFACT_NAME: baseline-${{ steps.getMostRecentRelease.outputs.VERSION }}android-artifact-apk + fetch-depth: 0 # Fetches the entire history - - name: Skip build if there's already an existing artifact for the baseline - if: ${{ fromJSON(steps.checkForExistingArtifact.outputs.ARTIFACT_FOUND) }} - run: echo 'APK for baseline ${{ steps.getMostRecentRelease.outputs.VERSION }} already exists, reusing existing build' + - 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" + echo "$previous_merge" + echo "BASELINE_REF=$previous_merge" >> "$GITHUB_OUTPUT" - name: Get pull request details id: getPullRequestDetails @@ -84,15 +72,14 @@ jobs: fi buildBaseline: - name: Build apk from latest release as a baseline + name: Build apk from baseline uses: ./.github/workflows/buildAndroid.yml needs: prep - if: ${{ !fromJSON(needs.prep.outputs.BASELINE_ARTIFACT_FOUND) }} secrets: inherit with: type: e2e - ref: ${{ needs.prep.outputs.BASELINE_VERSION }} - artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_VERSION }} + ref: ${{ needs.prep.outputs.BASELINE_REF }} + artifact-prefix: baseline-${{ needs.prep.outputs.BASELINE_REF }} buildDelta: name: Build apk from delta ref @@ -127,9 +114,6 @@ jobs: with: name: ${{ needs.buildBaseline.outputs.APK_ARTIFACT_NAME }} path: zip - # Set github-token only if the baseline was built in this workflow run: - github-token: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID && github.token }} - run-id: ${{ needs.prep.outputs.BASELINE_ARTIFACT_WORKFLOW_ID }} # The downloaded artifact will be a file named "app-e2e-release.apk" so we have to rename it - name: Rename baseline APK diff --git a/.nvmrc b/.nvmrc index b8e593f5210c..2a393af592b8 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.15.1 +20.18.0 diff --git a/.prettierignore b/.prettierignore index 98d06e8c5f71..b428978a1563 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,8 +18,7 @@ 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 +src/libs/SearchParser/autocompleteParser.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/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/android/app/build.gradle b/android/app/build.gradle index 40de69b4b7be..5ce4d0e5fddf 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 1009005101 - versionName "9.0.51-1" + versionCode 1009005401 + versionName "9.0.54-1" // 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..f476ad89c5b4 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) @@ -59,7 +64,7 @@ class MainApplication : MultiDexApplication(), ReactApplication { SoLoader.init(this, /* native exopackage */false) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. - load(bridgelessEnabled = false) + load() } if (BuildConfig.DEBUG) { FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(false) diff --git a/assets/images/companyCards/pending-bank.svg b/assets/images/companyCards/pending-bank.svg new file mode 100644 index 000000000000..dc265466d53f --- /dev/null +++ b/assets/images/companyCards/pending-bank.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/gallery-not-found.svg b/assets/images/gallery-not-found.svg new file mode 100644 index 000000000000..25da973ce9cb --- /dev/null +++ b/assets/images/gallery-not-found.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file 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/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index 91fc4b1bf528..2d8e27fd453e 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -227,8 +227,6 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): 'react-native-config': 'react-web-config', // eslint-disable-next-line @typescript-eslint/naming-convention 'react-native$': 'react-native-web', - // eslint-disable-next-line @typescript-eslint/naming-convention - 'react-native-sound': 'react-native-web-sound', // Module alias for web & desktop // https://webpack.js.org/configuration/resolve/#resolvealias // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 304811332916..e6660d848129 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -24,6 +24,7 @@ - [Refs](#refs) - [Other Expensify Resources on TypeScript](#other-expensify-resources-on-typescript) - [Default value for inexistent IDs](#default-value-for-inexistent-IDs) + - [Extract complex types](#extract-complex-types) - [Naming Conventions](#naming-conventions) - [Type names](#type-names) - [Prop callbacks](#prop-callbacks) @@ -492,6 +493,30 @@ const foo = report?.reportID ?? '-1'; report ? report.reportID : '-1'; ``` +### Extract complex types + +Advanced data types, such as objects within function parameters, should be separated into their own type definitions. Callbacks in function parameters should be extracted if there's a possibility they could be reused somewhere else. + +```ts +// BAD +function foo(param1: string, param2: {id: string}) {...}; + +// BAD +function foo(param1: string, param2: (value: string) => void) {...}; + +// GOOD +type Data = { + id: string; +}; + +function foo(param1: string, param2: Data) {...}; + +// GOOD +type Callback = (value: string) => void + +function foo(param1: string, param2: Callback) {...}; +``` + ## Naming Conventions ### Type names diff --git a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md b/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md deleted file mode 100644 index 7dcc8e5e9c29..000000000000 --- a/docs/articles/expensify-classic/connections/Additional-Travel-Integrations.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Importing Receipts from Various Platforms to Expensify -description: Detailed guide on how to import receipts from multiple travel platforms into Expensify. ---- - -# Overview -You can automatically import receipts from many travel platforms into Expensify, to make tracking expenses while traveling for business a breeze. Read on to learn how to import receipts from Bolt Work, Spot Hero, Trainline, Grab, HotelTonight, and Kayak for Business. - -## How to Connect to Bolt Work - -### Set Up Bolt Work Profile -- Open the Bolt app, go to the side navigation menu, and select Payment. -- At the bottom, select Set up work profile and follow the instructions, entering your work email for verification. - -### Link to Expensify -- In the Bolt app, go to Work Rides. -- Select Add expense provider, choose Expensify, and enter the associated email to receive a verification link. -- Ensure you select your work ride profile as the payment method before booking. - -## How to Connect to SpotHero - -### Set up a Business Profile -- Open the SpotHero app, click the hamburger icon, and go to Account Settings. -- Click Set up Business Profile. -- Specify the email connected to Expensify and set up your payment method. -- Upon checkout, choose between Business and Personal Profiles in the "Payment Details" section. -- If you want, you can set a weekly or monthly cadence for consolidated SpotHero expense reports in your Business Profile settings. This will batch all of your SpotHero expenses to import into Expensify at that cadence. - -## How to Connect to Trainline -- To send a ticket receipt to Expensify: - - In the Trainline app, navigate to the My Tickets tab. - - Tap Manage my booking > Expense receipt > Send to Expensify. -- That’s it! - -## How to Connect to Grab -- In the Grab app, tap on your name, go to “Profiles”, and “Add a business profile”. -- Follow instructions and enter your work email for verification. -- In your profile, tap on Business > Expense Solution > Expensify > Save. -- Before booking, select your Business profile and confirm. - -## How to Connect to HotelTonight -- In HotelTonight, go to the Bookings tab and select your booking. -- Select Receipt > Expensify, enter your Expensify email, and send. - -## How to Connect to Kayak for Business - -### Admin Setup -- Admins should go to “Company Settings” and click on “Connect to Expensify”. -- Bookings made by employees will automatically be sent to Expensify. - -### Traveler Setup -- From your account settings, choose whether expenses should be sent to Expensify automatically or manually. -- We recommend sending them automatically, so you can travel without even thinking about your expense reports. - -{% include faq-begin.md %} - -**Q: What if I don’t have the option for Send to Expensify in Trainline?** - -A: This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS share to Expensify function for Trainline receipts. - -**Q: Why should I choose automatic mode in Kayak for Business?** - -A: Automatic mode is less effort as it’s easier to delete an expense in Expensify than to remember to forward a forgotten receipt. - -**Q: Can I receive consolidated reports from SpotHero?** - -A: Yes, you can set a weekly or monthly cadence for SpotHero expenses to be emailed in a consolidated report. - -**Q: Do I need to select a specific profile before booking in Bolt Work and Grab?** - -A: Yes, ensure you have selected your work or business profile as the payment method before booking. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md new file mode 100644 index 000000000000..2e5b5065b3d5 --- /dev/null +++ b/docs/articles/expensify-classic/connections/Travel-receipt-integrations.md @@ -0,0 +1,121 @@ +--- +title: Travel Receipt Integrations +description: How to use pre-built or custom integrations to track travel expenses +--- + +Expensify’s receipt integrations allow a merchant to upload receipts directly to a user’s Expensify account. A merchant just has to email a receipt to an Expensify user and Cc receipts@expensify.com. This automatically creates a transaction in the Expensify account for the user whose email address is in the To field. + +You can set up a receipt integration by using one of our existing pre-built integrations, or by building your own receipt integration. + +## Use a pre-built travel integration + +You can use our pre-built integrations to automatically import travel receipts from Bolt Work, Spot Hero, Grab, and Kayak for Business. + +### Bolt Work + +1. In the Bolt app, tap the menu icon in the top left and tap **Work trips**. +2. Tap **Create profile**. +3. Enter the email address that you use for Expensify, then tap **Next**. +4. Enter your company details, then tap **Next**. +5. Choose a payment method. If you don’t want to use the existing payment methods, you can create a new one by tapping **Add Payment Method**. Then tap **Next**. +6. Tap **Done**. +7. Tap Add expense provider, then tap **Expensify**. +8. Tap **Verify**. +9. Tap the menu icon on the top left and tap **Work trips** once more. +10. Tap **Add expense provider** and select **Expensify** again. + +When booking a trip with Bolt Work, select your work trip profile as the payment method before booking. Then the receipt details will be automatically sent to Expensify. + +### SpotHero + +1. In the SpotHero app, tap the menu icon in the top left and tap **Account Settings**. +2. Tap **Set up Business Profile**. +3. Tap **Create Business Profile**. +4. Enter the email address you use for Expensify and tap **Next**. +5. Tap **Add a Payment Method** and enter your payment account details. Then tap **Next**. +6. Tap **Expensify**. + +When reserving parking with SpotHero, select your business profile in the Payment Details section. Then the receipt will be automatically sent to Expensify. In your SpotHero Business Profile settings, you can also set a weekly or monthly cadence for SpotHero to send a batch of expenses to Expensify. + +### Grab + +1. In the Grab app, tap your profile picture in the top left. +2. Tap your user icon again at the top of the settings menu. +3. Tap **Add a business profile**. +4. Tap Next twice, then tap **Let’s Get Started**. +5. Enter the email address you use for Expensify and tap the next arrow in the bottom right. +6. Check your email and copy the verification code you receive from Grab. +7. Tap **Manage My Business Profile**. +8. Under Preferences, tap **Expense Solution**. +9. Tap **Expensify**, then tap **Save**. + +When booking a trip with Grab, tap **personal** and select **business** to ensure your business profile is selected. Then the receipt will be automatically sent to Expensify. + +### KAYAK for Business + +**Admin Setup** + +This process must be completed by a KAYAK for Business admin. + +1. On your KAYAK for Business homepage, click **Company Settings**. +2. Click **Connect to Expensify**. + +KAYAK for Business will now forward bookings made by each employee into Expensify. + +**Traveler Setup** + +1. On your KAYAK for Business homepage, click **Profile Account Settings**. +2. Enable the Expensify toggle to have your expenses automatically sent to Expensify. You also have the option to send them manually. + +## Build your own receipt integration + +1. Email receiptintegration@expensify.com and include: + - **Subject**: Use “Receipt Integration Request" as the subject line + - **Body**: List all email addresses the merchant sends email receipts from +2. Once you receive your email confirmation (within approximately 2 weeks) that the email addresses have been whitelisted, you’ll then be able to Cc receipts@expensify.com on receipt emails to users, and transactions will be created in the users’ Expensify account. +3. Test the integration by sending a receipt email to the email address you used to create your Expensify account and Cc receipts@expensify.com. Wait for the receipt to be SmartScanned. Then you will see the merchant, date, and amount added to the transaction. + +### Using the integration + +When sending an emailed receipt: + +- Attachments on an email (that are not an .ics file) will be SmartScanned. We recommend including the receipt as the only attachment. +- You can only include one email address in the To field. In the Cc field, include only receipts@expensify.com. +- Reservations for hotels and car rentals cannot be sent to Expensify as an expense because they are paid at the end of usage. You can only send transaction data for purchases that have already been made. +- Use standardized three-letter currency codes (ISO 4217) where applicable. + +{% include faq-begin.md %} + +**In Trainline, what if I don’t have the option for Send to Expensify?** + +This can happen if the native iOS Mail app is not installed on an Apple device. However, you can still use the native iOS Share to Expensify function for Trainline receipts. + +**Why does it take 2 weeks to set up a custom integration?** + +Receipt integrations require our engineers to manually set them up on the backend. For that reason, it can take up to 2 weeks to set it up. + +**Is there a way to connect via API?** + +No, at this time there are no API receipt integrations. All receipt integrations are managed via receipt emails. + +**What is your Open API?** + +Our Open API is a self-serve tool meant to pull information out of Expensify. Typically, this tool is used to build integrations with accounting solutions that we don’t directly integrate with. If you wish to push data into Expensify, the only way to integrate is via the receipt integration options listed above in this article. + +**Are you able to split one email into separate receipts?** + +The receipt integration is unable to automatically split one email into separate receipts. However, once the receipt is SmartScanned, users can [split the expense](https://help.expensify.com/articles/expensify-classic/expenses/Split-an-expense) in their Expensify account. + +**Can we set up a (co-marketing) partnership?** + +We currently do not offer any co-marketing partnerships. + +**Can we announce or advertise our custom integration with Expensify?** + +Absolutely! You can promote the integration across your social media channels (tag @expensify and use the #expensify hashtag) and you can even create your own dedicated landing page on your website for your integration. At a minimum, we recommend including a brief overview of how the integration works, the benefits of using it, an integration setup guide, and guidance for how someone can contact you for support or integration setup if necessary. + +**How can I get help?** + +You can contact Concierge for ongoing support any time by clicking the green chat icon in the mobile or web app, or by emailing concierge@expensify.com. Concierge is a global team of highly trained product specialists focused on making our product as easy to use as possible and answering all your questions. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md index f926792ffd1f..aecf21acfc3f 100644 --- a/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/expensify-classic/connections/netsuite/Configure-Netsuite.md @@ -2,70 +2,62 @@ title: Configure Netsuite description: Configure NetSuite's export, coding, and advanced settings. --- -By correctly configuring your NetSuite settings in Expensify, you can leverage the connection's settings to automate most of the tasks, making your workflow more efficient. +Correctly configuring NetSuite settings in Expensify ensures seamless integration between your expense management and accounting processes, saving time and reducing manual errors. Aligning your workspace settings with NetSuite’s financial structure can automate data syncs, simplify reporting, and improve overall financial accuracy. + +# Best Practices Using NetSuite +A connection to NetSuite lets you combine the power of Expensify’s expense management features with NetSuite’s accounting capabilities. + +By following the recommended best practices below, your finances will be automatically categorized and accounted for in NetSuite: +- Configure your setup immediately after making the connection, and review each settings tab thoroughly. +- Keep Auto Sync enabled: + - The daily sync will update Expensify with any changes to your chart of accounts, customers/projects, or bank accounts in NetSuite. + - Finalized reports will be exported to NetSuite automatically, saving your admin team time with every report. +- Set your preferred exporter to someone who is both a workspace and domain admin. +- Configure your coding settings and enforce them by [requiring categories and tags on expenses](https://help.expensify.com/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses). # Step 1: Configure Export Settings There are numerous options for exporting Expensify reports to NetSuite. Let's explore how to configure these settings to align with your business needs. -To access these settings, head to **Settings > Workspace > Group > Connections** and select the **Configure** button. +To access these settings, go to **Settings > Workspace > Group > Connections** and select the **Configure** button. -## Export Options - -### Subsidiary +## Subsidiary The subsidiary selection will only appear if you use NetSuite OneWorld and have multiple subsidiaries active. If you add a new subsidiary to NetSuite, sync the workspace connection, and the new subsidiary should appear in the dropdown list under **Settings > Workspaces > _[Workspace Name]_ > Connections**. -### Preferred Exporter +## Preferred Exporter This option allows any admin to export, but the preferred exporter will receive notifications in Expensify regarding the status of exports. -### Date +## Date The three options for the date your report will export with are: - Date of last expense: This will use the date of the previous expense on the report - Submitted date: The date the employee submitted the report - Exported date: The date you export the report to NetSuite -## Reimbursable Expenses - -### Expense Reports - -Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite. - -### Vendor Bills - -Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding policy. Each report will be posted as payable to the vendor associated with the employee who submitted the report. -You can also set an approval level in NetSuite for vendor bills. +## Export Settings for Reimbursable Expenses -### Journal Entries +**Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite. -Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this policy. All the transactions will be posted to the payable account specified in the policy. +**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding policy. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills. -You can also set an approval level in NetSuite for the journal entries. +**Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this policy. All the transactions will be posted to the payable account specified in the policy. You can also set an approval level in NetSuite for the journal entries. -**Important Notes:** - Journal entry forms by default do not contain a customer column, so it is not possible to export customers or projects with this export option - The credit line and header level classifications are pulled from the employee record -## Non-Reimbursable Expenses +## Export Settings for Non-Reimbursable Expenses -### Vendor Bills +**Vendor Bills:** Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills. -Non-reimbursable expenses will be posted as a vendor bill payable to the default vendor specified in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific vendor in NetSuite. You can also set an approval level in NetSuite for the bills. +**Journal Entries:** Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. -### Journal Entries - -Non-reimbursable expenses will be posted to the Journal Entries posting account selected in your policy's connection settings. If you centrally manage your company cards through Domains, you can export expenses from each card to a specific account in NetSuite. - -**Important Notes:** - Expensify Card expenses will always export as Journal Entries, even if you have Expense Reports or Vendor Bills configured for non-reimbursable expenses on the Export tab - Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option - The credit line and header level classifications are pulled from the employee record -### Expense Reports - -To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite. +**Expense Reports:** To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite. To use a default corporate card for non-reimbursable expenses, you must select the correct card on the employee records (for individual accounts) or the subsidiary record (If you use a non-one world account, the default is found in your accounting preferences). @@ -77,11 +69,11 @@ Add the corporate card option and corporate card main field to your expense repo You can select the default account on your employee record to use individual corporate cards for each employee. Make sure you add this field to your employee entity form in NetSuite. If you have multiple cards assigned to a single employee, you cannot export to each account. You can only have a single default per employee record. -### Export Invoices +## Export Invoices Select the Accounts Receivable account you want your Invoice Reports to export. In NetSuite, the Invoices are linked to the customer, corresponding to the email address where the Invoice was sent. -### Default Vendor Bills +## Default Vendor Bills When selecting the option to export non-reimbursable expenses as vendor bills, the list of vendors will be available in the dropdown menu. @@ -169,7 +161,7 @@ From there, you should see the values for the Custom Segment under the Tag or Re Don’t use the "Filtered by" feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to "Subsidiary" and enabling all subsidiaries to ensure you don't receive any errors upon exporting reports. -### Custom Records +## Custom Records Custom Records are added through the Custom Segments feature. @@ -197,7 +189,7 @@ Lastly, head over to Expensify and do the following: From there, you should see the values for the Custom Records under the Tag or Report Field settings in Expensify. -### Custom Lists +## Custom Lists To add Custom Lists to your workspace, you’ll need to locate two fields in NetSuite: - The name of the record @@ -250,17 +242,11 @@ With this enabled, all submitters can add any newly imported Categories to an Ex ## Invite Employees & Set Approval Workflow -### Invite Employees - -Use this option in Expensify to bring your employees from a specific NetSuite subsidiary into Expensify. -Once imported, Expensify will send them an email letting them know they've been added to a workspace. +**Invite Employees:** Use this option in Expensify to bring your employees from a specific NetSuite subsidiary into Expensify. Once imported, Expensify will send them an email letting them know they've been added to a workspace. -### Set Approval Workflow - -Besides inviting employees, you can also establish an approval process in NetSuite. - -By doing this, the Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval. +**Set Approval Workflow:** In addition to inviting employees, you can establish an approval process in NetSuite. The Approval Workflow in Expensify will automatically follow the same rules as NetSuite, typically starting with Manager Approval. +The available options are: - **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Manager Approval (default):** Two levels of approval route reports first to an employee's NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace's People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > _[Workspace Name]_ > People page**. You can set a user role for each new employee and enforce an approval workflow. @@ -275,7 +261,7 @@ Using this feature allows you to send the original amount of the expense rather ## Cross-Subsidiary Customers/Projects -This allows you to import Customers and Projects across all subsidiaries to a single group workspace. For this functionality, you must enable "Intercompany Time and Expense" in NetSuite. +This allows you to import Customers and Projects across all subsidiaries to a single group workspace. To enable this functionality in NetSuite, you must enable "Intercompany Time and Expense." That feature is found in NetSuite under _Setup > Company > Setup Tasks: Enable Features > Advanced Features_. @@ -303,7 +289,7 @@ If you have Approval Routing selected in your accounting preference, this will o If you do not wish to use Approval Routing in NetSuite, go to _Setup > Accounting > Accounting Preferences > Approval Routing_ and ensure Vendor Bills and Journal Entries are not selected. -### Collection Account +## Collection Account When exporting invoices, once marked as Paid, the payment is marked against the account selected after enabling the Collection Account setting. @@ -343,7 +329,7 @@ Add the corporate card option and the corporate card main field to configure you If you prefer individual corporate cards for each employee, you can select the default account on the employee record. Add this field to your employee entity form in NetSuite (under _Customize > Customize Form_ from any employee record). Note that each employee can have only one corporate card account default. -### Exporting Company Cards to GL Accounts in NetSuite +## Exporting Company Cards to GL Accounts in NetSuite If you need to export company card transactions to individual GL accounts, you can set that up at the domain level. @@ -359,9 +345,7 @@ You’ll want to set up Tax Groups in Expensify if you're keeping track of taxes Expensify can import "NetSuite Tax Groups" (not Tax Codes) from NetSuite. Tax Groups can contain one or more Tax Codes. If you have subsidiaries in the UK or Ireland, ensure your Tax Groups have only one Tax Code. -You can locate these in NetSuite by setting up> Accounting > Tax Groups. - -You’ll want to name Tax Groups something that makes sense to your employees since both the name and the tax rate will appear in Expensify. +You can locate these in NetSuite by setting up> Accounting > Tax Groups. Name the Tax Groups something that makes sense to your employees since both the name and the tax rate will appear in Expensify. To bring NetSuite Tax Groups into Expensify, here's what you need to do: 1. Create your Tax Groups in NetSuite by going to _Setup > Accounting > Tax Groups_ @@ -386,7 +370,7 @@ Expensify. If you deactivate this group in NetSuite, it will lead to export erro Additionally, some tax nexuses in NetSuite have specific settings that need to be configured in a certain way to work seamlessly with the Expensify integration: - ​​In the Tax Code Lists Include field, choose "Tax Groups" or "Tax Groups and Tax Codes." This setting determines how tax information is handled. -- In the Tax Rounding Method field, select "Round Off." Although it won't cause connection errors, not using this setting can result in exported amounts differing from what NetSuite expects. +- In the Tax Rounding Method field, select "Round Off." Although this setting won't cause connection errors, not using it can result in exported amounts differing from what NetSuite expects. If your tax groups are importing into Expensify but not exporting to NetSuite, check that each tax group has the right subsidiaries enabled. That is crucial for proper data exchange. @@ -408,7 +392,7 @@ Let's dive right in: 1. Access Configuration Settings: Go to **Settings > Workspace > Group > _[Workspace Name]_ > Connections > Configuration** 2. Choose Your Accounts Receivable Account: Scroll down to "Export Expenses to" and select the appropriate Accounts Receivable account from the dropdown list. If you don't see any options, try syncing your NetSuite connection by returning to the Connections page and clicking **Sync Now** -### Exporting an Invoice to NetSuite +## Exporting an Invoice to NetSuite Invoices will be automatically sent to NetSuite when they are in the "Processing" or "Paid" status. This ensures you always have an up-to-date record of unpaid and paid invoices. @@ -421,7 +405,7 @@ When exporting to NetSuite, we match the recipient's email address on the invoic Once exported, the invoice will appear in the Accounts Receivable account you selected during your NetSuite Export configuration. -### Updating the status of an invoice to "paid" +## Updating the status of an invoice to "paid" When you mark an invoice as "Paid" in Expensify, this status will automatically update in NetSuite. Similarly, if the invoice is marked as "Paid" in NetSuite, it will sync with Expensify. The payment will be reflected in the Collection account specified in your Advanced Settings Configuration. 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/expensify-classic/workspaces/Personal-and-Corporate-Karma.md b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md new file mode 100644 index 000000000000..5c146b279163 --- /dev/null +++ b/docs/articles/expensify-classic/workspaces/Personal-and-Corporate-Karma.md @@ -0,0 +1,42 @@ +--- +title: Personal and Corporate Karma +description: Details about Personal and Corporate Karma +--- + +# Overview + +Expensify.org empowers individuals and communities to eliminate injustice around the world by making giving and volunteering more convenient, meaningful, and collaborative. + +## What is the Expensify.org giving model + +[Expensify.org](https://www.expensify.org/about) is built on creating a transparent and convenient way to create an emotional connection between donors, volunteers, and recipients. + +## Where do Expensify.org funds come from? + +Corporate Karma, Personal Karma, and monetary donations. + +## What is Personal Karma? + +Personal Karma allows individual users to automatically donate a small percentage of their monthly added expenses to Expensify.org. + +For every $500 of expenses added, you’ll donate $1 to a related Expensify.org fund. All reported and unreported expenses, including invoice expenses, on the Expenses page are calculated to get the donation amount. Each month, Expensify will charge the billing card on file for the donation amount, and you’ll receive a donation receipt via email. + +The fund from your Personal Karma is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. + +## What is Corporate Karma? + +Corporate Karma is for companies that want to engage in social responsibility. Each month, the donation is calculated based on the total amount of all approved expense reports, including invoices, across all Workspace. + +For every $500 your team spends monthly, your company will donate $1 to a related Expensify.org fund. Expensify will charge the payment card on file for the donation amount each month, and you’ll receive a donation receipt via email. + +The fund to which your Corporate Karma goes is determined by the expense's MCC (Merchant Category Code). Each MCC supports one of Expensify.org's funds: Climate Justice, Food Security, Housing Equity, Reentry Services, and Youth Advocacy. + +{% include faq-begin.md %} + +**How do I opt-in to Personal or Corporate Karma donations?** + +You can donate Personal and Corporate Karma to Expensify.org in your company or personal workspace settings. + +Go to **Settings** > **Workspaces** > click on your Individual or Group workspace settings and Opt-in to Karma donations. + +{% include faq-end.md %} diff --git a/docs/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.md b/docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md similarity index 63% rename from docs/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.md rename to docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md index d30fa06bc059..c181536d1174 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/adding-payment-card-subscription-overview.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription.md @@ -1,15 +1,18 @@ -Subscription Management +--- +title: Subscription Management +description: How to manage your subscription +--- Under the subscriptions section of your account, you can manage your payment card details, view your current plan, add a billing card, and adjust your subscription size and renewal date. To view or manage your subscription in New Expensify: -**Open the App**: Launch New Expensify on your device. -**Go to Account Settings**: Click your profile icon in the bottom-left corner. -**Find Workspaces**: Navigate to the Workspaces section. -**Open Subscriptions**: Click Subscription under Workspaces to view your subscription. +* **Open the App**: Launch New Expensify on your device. +* **Go to Account Settings**: Click your profile icon in the bottom-left corner. +* **Find Workspaces**: Navigate to the Workspaces section. +* **Open Subscriptions**: Click Subscription under Workspaces to view your subscription. ## Add a Payment Card Look for the option to **Add Payment Card**. Enter your payment card details securely to ensure uninterrupted service. -[PLACEHOLDER for design image- default] +![A screenshot of adding payment card]({{site.url}}/assets/images/ExpensifyHelp-Subscription-Default.png){:width="100%"} ## Subscription Overview This is where you can view your current subscription plan and see details like the number of seats, billing information, and the next renewal date. @@ -19,13 +22,13 @@ This is where you can view your current subscription plan and see details like t - **Auto-increase annual seats**: Here you can see how much you could save by automatically increasing seats to accommodate team members who exceed the current subscription size. **Note**: This will extend your annual subscription end date. -[PLACEHOLDER for design image- your plan] +![A screenshot of subscription details]({{site.url}}/assets/images/ExpensifyHelp-Subscription-Details.png){:width="100%"} ## Early Cancellation Requests If you need to cancel your subscription early, you can find the **Request Early Cancellation** option in the same Subscriptions section. **Note**: Not all customers are eligible to cancel their subscription early. -[PLACEHOLDER for design image- billing] +![A screenshot of cancellation button]({{site.url}}/assets/images/ExpensifyHelp-Subscription-Billing.png){:width="100%"} ## Pricing Information For more details on pricing plans, visit Billing Page [coming soon!] 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. - -
- -# 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. - -
-{% 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 %} - -
    - -
- -# 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. - -
- {% 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/articles/new-expensify/expenses-&-payments/Track-expenses.md b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md index f6260b9f8f84..77256279b1d7 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md +++ b/docs/articles/new-expensify/expenses-&-payments/Track-expenses.md @@ -40,4 +40,6 @@ For an in-depth walkthrough on how to create an expense, check out the [create a {% include end-selector.html %} +![The New Expensify page is open with the FAB (big + button) clicked and the option to Track Expenses is highlighted.]({{site.url}}/assets/images/FAB_track_expense.png){:width="100%"} + diff --git a/docs/articles/new-expensify/workspaces/Create-expense-categories.md b/docs/articles/new-expensify/workspaces/Create-expense-categories.md index 56557d449908..a6874ac0a2ef 100644 --- a/docs/articles/new-expensify/workspaces/Create-expense-categories.md +++ b/docs/articles/new-expensify/workspaces/Create-expense-categories.md @@ -110,6 +110,7 @@ GL codes and payroll codes can be exported to a CSV export. They are not display 6. To add or edit a GL code, click the GL code field, make the desired change, then click **Save** 7. To add or edit a payroll code, click the payroll code field, make the desired change, then click **Save** +![In the Workspace > Categories setting, the right-hand panel is open and the GL and Payroll code setting is highlighted.]({{site.url}}/assets/images/workspace_gl_payroll_codes.png){:width="100%"} # Apply categories to expenses automatically diff --git a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md index 8f2cf0897ad0..df77ed3b5b01 100644 --- a/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md +++ b/docs/articles/new-expensify/workspaces/Require-tags-and-categories-for-expenses.md @@ -29,6 +29,8 @@ To require workspace members to add tags and/or categories to their expenses, {% include end-option.html %} {% include end-selector.html %} + +![In the Workspace > Categories setting, the right-hand panel is open and the toggle to require categories on expenses is highlighted.]({{site.url}}/assets/images/workspace_category_toggle.png){:width="100%"} This will highlight the tag and/or category field as required on all expenses. 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/FAB_track_expense.png b/docs/assets/images/FAB_track_expense.png new file mode 100644 index 000000000000..6ee0cf5abba4 Binary files /dev/null and b/docs/assets/images/FAB_track_expense.png differ diff --git a/docs/assets/images/Workspace_category_toggle.png b/docs/assets/images/Workspace_category_toggle.png new file mode 100644 index 000000000000..c6af6fe183c0 Binary files /dev/null and b/docs/assets/images/Workspace_category_toggle.png differ diff --git a/docs/assets/images/cardfeeds-01.png b/docs/assets/images/cardfeeds-01.png new file mode 100644 index 000000000000..ddf318fc05e8 Binary files /dev/null and b/docs/assets/images/cardfeeds-01.png differ diff --git a/docs/assets/images/cardfeeds-02.png b/docs/assets/images/cardfeeds-02.png new file mode 100644 index 000000000000..b0f047722444 Binary files /dev/null and b/docs/assets/images/cardfeeds-02.png differ diff --git a/docs/assets/images/compcard-01.png b/docs/assets/images/compcard-01.png new file mode 100644 index 000000000000..95b577714833 Binary files /dev/null and b/docs/assets/images/compcard-01.png differ diff --git a/docs/assets/images/compcard-02.png b/docs/assets/images/compcard-02.png new file mode 100644 index 000000000000..a34cdbfa1603 Binary files /dev/null and b/docs/assets/images/compcard-02.png differ diff --git a/docs/assets/images/compcard-03.png b/docs/assets/images/compcard-03.png new file mode 100644 index 000000000000..1e4bb6776e17 Binary files /dev/null and b/docs/assets/images/compcard-03.png differ diff --git a/docs/assets/images/csv-01.png b/docs/assets/images/csv-01.png new file mode 100644 index 000000000000..e6cfe9cf36f6 Binary files /dev/null and b/docs/assets/images/csv-01.png differ diff --git a/docs/assets/images/csv-02.png b/docs/assets/images/csv-02.png new file mode 100644 index 000000000000..72ba2b5cf583 Binary files /dev/null and b/docs/assets/images/csv-02.png differ diff --git a/docs/assets/images/csv-03.png b/docs/assets/images/csv-03.png new file mode 100644 index 000000000000..4aac1f72893c Binary files /dev/null and b/docs/assets/images/csv-03.png differ diff --git a/docs/assets/images/expenses-01.png b/docs/assets/images/expenses-01.png new file mode 100644 index 000000000000..0169a20b2e2b Binary files /dev/null and b/docs/assets/images/expenses-01.png differ diff --git a/docs/assets/images/expenses-02.png b/docs/assets/images/expenses-02.png new file mode 100644 index 000000000000..1164f341b033 Binary files /dev/null and b/docs/assets/images/expenses-02.png differ diff --git a/docs/assets/images/expenses-03.png b/docs/assets/images/expenses-03.png new file mode 100644 index 000000000000..75c06639cb81 Binary files /dev/null and b/docs/assets/images/expenses-03.png differ diff --git a/docs/assets/images/expenses-04.png b/docs/assets/images/expenses-04.png new file mode 100644 index 000000000000..16e9b9756d47 Binary files /dev/null and b/docs/assets/images/expenses-04.png differ diff --git a/docs/assets/images/expenses-05.png b/docs/assets/images/expenses-05.png new file mode 100644 index 000000000000..cf99d05eb1af Binary files /dev/null and b/docs/assets/images/expenses-05.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/assets/images/tax_tracking-01.png b/docs/assets/images/tax_tracking-01.png new file mode 100644 index 000000000000..a35da6c1848a Binary files /dev/null and b/docs/assets/images/tax_tracking-01.png differ diff --git a/docs/assets/images/tax_tracking-02.png b/docs/assets/images/tax_tracking-02.png new file mode 100644 index 000000000000..4d3df9eda60c Binary files /dev/null and b/docs/assets/images/tax_tracking-02.png differ diff --git a/docs/assets/images/workspace_gl_payroll_codes.png b/docs/assets/images/workspace_gl_payroll_codes.png new file mode 100644 index 000000000000..6b7770dc01b0 Binary files /dev/null and b/docs/assets/images/workspace_gl_payroll_codes.png differ diff --git a/docs/redirects.csv b/docs/redirects.csv index 90baeff59260..a7d4d94adb5d 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -571,6 +571,7 @@ https://community.expensify.com/discussion/4641/how-to-add-a-deposit-only-bank-a https://community.expensify.com/discussion/5940/how-to-get-reimbursed-outside-the-us-with-wise-for-non-us-employees,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Get-reimbursed-faster-as-a-non-US-employee https://help.expensify.com/articles/expensify-classic/spending-insights,https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-notifications,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Set-Notifications +https://help.expensify.com/articles/expensify-classic/connections/Additional-Travel-Integrations,https://help.expensify.com/articles/expensify-classic/connections/Travel-receipt-integrations https://help.expensify.com/articles/new-expensify/getting-started/Upgrade-to-a-Collect-Plan,https://help.expensify.com/Hidden/Upgrade-to-a-Collect-Plan https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports-Invoices-and-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Reimburse-Reports https://help.expensify.com/articles/new-expensify/expenses-&-payments/pay-an-invoice.html,https://help.expensify.com/articles/new-expensify/expenses-&-payments/Pay-an-invoice @@ -584,6 +585,7 @@ 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/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 +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 diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg index 76307ce1b460..567a867981e6 100644 Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ diff --git a/ios/NewApp_Development.mobileprovision.gpg b/ios/NewApp_Development.mobileprovision.gpg new file mode 100644 index 000000000000..34f034752b7f Binary files /dev/null and b/ios/NewApp_Development.mobileprovision.gpg differ 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 36a986b37b89..361113013ae4 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.51 + 9.0.54 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.51.1 + 9.0.54.1 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 f5bd7a20c34a..cb867d7af0b5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.51 + 9.0.54 CFBundleSignature ???? CFBundleVersion - 9.0.51.1 + 9.0.54.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7c5da25860f0..c7c9879bb2ab 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.51 + 9.0.54 CFBundleVersion - 9.0.51.1 + 9.0.54.1 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 2ef609e9ef94..7202ef76ab66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "new.expensify", - "version": "9.0.51-1", + "version": "9.0.54-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.51-1", + "version": "9.0.54-1", "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", @@ -51,13 +51,14 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.94", + "expensify-common": "2.0.100", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", "expo-image-manipulator": "12.0.5", "fast-equals": "^4.0.3", "focus-trap-react": "^10.2.3", + "howler": "^2.2.4", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "lodash-es": "4.17.21", @@ -76,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", @@ -116,7 +118,6 @@ "react-native-view-shot": "3.8.0", "react-native-vision-camera": "4.0.0-beta.13", "react-native-web": "^0.19.12", - "react-native-web-sound": "^0.1.3", "react-native-webview": "13.8.6", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", @@ -174,6 +175,7 @@ "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", + "@types/howler": "^2.2.12", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", "@types/js-yaml": "^4.0.5", @@ -202,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", @@ -217,13 +219,13 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.60", + "eslint-config-expensify": "^2.0.66", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "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", @@ -246,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", @@ -273,16 +275,8 @@ "xlsx": "file:vendor/xlsx-0.20.3.tgz" }, "engines": { - "node": "20.15.1", - "npm": "10.7.0" - } - }, - "lib/react-compiler-runtime": { - "version": "0.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "react": "18.3.1" + "node": "20.18.0", + "npm": "10.8.2" } }, "node_modules/@actions/core": { @@ -613,9 +607,10 @@ } }, "node_modules/@babel/eslint-parser": { - "version": "7.24.7", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.8.tgz", + "integrity": "sha512-Po3VLMN7fJtv0nsOjBDSbO1J71UhzShE9MuOSkWEV9IZQXzhZklYtzKZ8ZD/Ij3a0JBv1AG3Ny2L3jvAHQVOGg==", "dev": true, - "license": "MIT", "dependencies": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", @@ -3635,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", @@ -15781,6 +15777,12 @@ "hoist-non-react-statics": "^3.3.0" } }, + "node_modules/@types/howler": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.12.tgz", + "integrity": "sha512-hy769UICzOSdK0Kn1FBk4gN+lswcj1EKRkmiDtMkUGvFfYJzgaDXmVXkSShS2m89ERAatGIPnTUlp2HhfkVo5g==", + "dev": true + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "dev": true, @@ -17975,33 +17977,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/babel-eslint": { - "version": "10.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - }, - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "eslint": ">= 4.12.1" - } - }, - "node_modules/babel-eslint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, "node_modules/babel-jest": { "version": "29.4.1", "dev": true, @@ -18363,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", @@ -22824,16 +22800,16 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.60", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.60.tgz", - "integrity": "sha512-VlulvhEasWeX2g+AXC4P91KA9czzX+aI3VSdJlZwm99GLOdfv7mM0JyO8vbqomjWNUxvLyJeJjmI02t2+fL/5Q==", + "version": "2.0.66", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.66.tgz", + "integrity": "sha512-6L9EIAiOxZnqOcFEsIwEUmX0fvglvboyqQh7LTqy+1O2h2W3mmrMSx87ymXeyFMg1nJQtqkFnrLv5ENGS0QC3Q==", "dev": true, "dependencies": { + "@babel/eslint-parser": "^7.25.7", "@lwc/eslint-plugin-lwc": "^1.7.2", "@typescript-eslint/parser": "^7.12.0", "@typescript-eslint/rule-tester": "^7.16.1", "@typescript-eslint/utils": "^7.12.0", - "babel-eslint": "^10.1.0", "eslint": "^8.56.0", "eslint-config-airbnb": "19.0.4", "eslint-config-airbnb-base": "15.0.0", @@ -23549,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", @@ -24073,9 +24050,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.94", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.94.tgz", - "integrity": "sha512-Cco5X6u4IL5aQlFqa2IgGgR+vAffYLxpPN2d7bzfptW/pRLY2L2JRJohgvXEswlCcTKFVt4nIJ4bx9YIOvzxBA==", + "version": "2.0.100", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.100.tgz", + "integrity": "sha512-mektI+OuTywYU47Valjsn2+kLQ1/Wc9sWCY1/a0Vo8IHTXroQWvbKs5IXlkiqODi4SRonVZwOL3ha/oJD7o7nQ==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -25995,7 +25972,8 @@ }, "node_modules/howler": { "version": "2.2.4", - "license": "MIT" + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==" }, "node_modules/hpack.js": { "version": "2.1.6", @@ -34093,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", @@ -34178,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", @@ -34418,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", @@ -35419,8 +35415,8 @@ "underscore": "^1.13.6" }, "engines": { - "node": ">=20.15.1", - "npm": ">=10.7.0" + "node": ">=20.18.0", + "npm": ">=10.8.2" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -35747,16 +35743,6 @@ "react-dom": "^18.0.0" } }, - "node_modules/react-native-web-sound": { - "version": "0.1.3", - "license": "MIT", - "dependencies": { - "howler": "^2.2.1" - }, - "peerDependencies": { - "react-native-web": "*" - } - }, "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "license": "MIT" diff --git a/package.json b/package.json index bad9d3b31469..f0425a747967 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.51-1", + "version": "9.0.54-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -61,12 +61,13 @@ "e2e-test-runner-build": "node --max-old-space-size=8192 node_modules/.bin/ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/", "react-compiler-healthcheck": "react-compiler-healthcheck --verbose", "react-compiler-healthcheck-test": "react-compiler-healthcheck --verbose &> react-compiler-output.txt", - "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy ", + "generate-search-parser": "peggy --format es -o src/libs/SearchParser/searchParser.js src/libs/SearchParser/searchParser.peggy src/libs/SearchParser/baseRules.peggy", + "generate-autocomplete-parser": "peggy --format es -o src/libs/SearchParser/autocompleteParser.js src/libs/SearchParser/autocompleteParser.peggy src/libs/SearchParser/baseRules.peggy", "web:prod": "http-server ./dist --cors" }, "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", @@ -106,13 +107,14 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.94", + "expensify-common": "2.0.100", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", "expo-image-manipulator": "12.0.5", "fast-equals": "^4.0.3", "focus-trap-react": "^10.2.3", + "howler": "^2.2.4", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "lodash-es": "4.17.21", @@ -131,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", @@ -171,7 +174,6 @@ "react-native-view-shot": "3.8.0", "react-native-vision-camera": "4.0.0-beta.13", "react-native-web": "^0.19.12", - "react-native-web-sound": "^0.1.3", "react-native-webview": "13.8.6", "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", @@ -229,6 +231,7 @@ "@types/base-64": "^1.0.2", "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", + "@types/howler": "^2.2.12", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", "@types/js-yaml": "^4.0.5", @@ -257,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", @@ -272,13 +275,13 @@ "electron-builder": "25.0.0", "eslint": "^8.57.0", "eslint-config-airbnb-typescript": "^18.0.0", - "eslint-config-expensify": "^2.0.60", + "eslint-config-expensify": "^2.0.66", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^3.0.0", "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", @@ -301,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", @@ -374,7 +377,7 @@ ] }, "engines": { - "node": "20.15.1", - "npm": "10.7.0" + "node": "20.18.0", + "npm": "10.8.2" } } 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..a085cdbcfbe2 --- /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..23d467d 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)); + + 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-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 84003710938a..d5680ce34b6f 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', @@ -471,6 +474,19 @@ const CONST = { PERSONAL: 'PERSONAL', }, }, + NON_USD_BANK_ACCOUNT: { + STEP: { + COUNTRY: 'CountryStep', + BANK_INFO: 'BankInfoStep', + BUSINESS_INFO: 'BusinessInfoStep', + BENEFICIAL_OWNER_INFO: 'BeneficialOwnerInfoStep', + SIGNER_INFO: 'SignerInfoStep', + AGREEMENTS: 'AgreementsStep', + FINISH: 'FinishStep', + }, + STEP_NAMES: ['1', '2', '3', '4', '5', '6'], + STEP_HEADER_HEIGHT: 40, + }, INCORPORATION_TYPES: { LLC: 'LLC', CORPORATION: 'Corp', @@ -1564,6 +1580,7 @@ const CONST = { TRACKING_CATEGORY_OPTIONS: { DEFAULT: 'DEFAULT', TAG: 'TAG', + REPORT_FIELD: 'REPORT_FIELD', }, }, @@ -1616,6 +1633,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', @@ -2529,6 +2552,13 @@ const CONST = { VISA: 'vcf', AMEX: 'gl1025', STRIPE: 'stripe', + CITIBANK: 'oauth.citibank.com', + CAPITAL_ONE: 'oauth.capitalone.com', + BANK_OF_AMERICA: 'oauth.bankofamerica.com', + CHASE: 'oauth.chase.com', + BREX: 'oauth.brex.com', + WELLS_FARGO: 'oauth.wellsfargo.com', + AMEX_DIRECT: 'oauth.americanexpressfdx.com', }, STEP_NAMES: ['1', '2', '3', '4'], STEP: { @@ -2582,6 +2612,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', @@ -2616,6 +2647,15 @@ const CONST = { WELLS_FARGO: 'Wells Fargo', OTHER: 'Other', }, + BANK_CONNECTIONS: { + WELLS_FARGO: 'wellsfargo', + BANK_OF_AMERICA: 'bankofamerica', + CHASE: 'chase', + BREX: 'brex', + CAPITAL_ONE: 'capitalone', + CITI_BANK: 'citibank', + AMEX: 'americanexpressfdx', + }, AMEX_CUSTOM_FEED: { CORPORATE: 'American Express Corporate Cards', BUSINESS: 'American Express Business Cards', @@ -5704,6 +5744,7 @@ const CONST = { KEYWORD: 'keyword', IN: 'in', }, + EMPTY_VALUE: 'none', }, REFERRER: { @@ -5905,6 +5946,21 @@ const CONST = { // The timeout duration (1 minute) (in milliseconds) before the window reloads due to an error. ERROR_WINDOW_RELOAD_TIMEOUT: 60000, + INDICATOR_STATUS: { + HAS_USER_WALLET_ERRORS: 'hasUserWalletErrors', + HAS_PAYMENT_METHOD_ERROR: 'hasPaymentMethodError', + HAS_POLICY_ERRORS: 'hasPolicyError', + HAS_CUSTOM_UNITS_ERROR: 'hasCustomUnitsError', + HAS_EMPLOYEE_LIST_ERROR: 'hasEmployeeListError', + HAS_SYNC_ERRORS: 'hasSyncError', + HAS_SUBSCRIPTION_ERRORS: 'hasSubscriptionError', + HAS_REIMBURSEMENT_ACCOUNT_ERRORS: 'hasReimbursementAccountErrors', + HAS_LOGIN_LIST_ERROR: 'hasLoginListError', + HAS_WALLET_TERMS_ERRORS: 'hasWalletTermsErrors', + HAS_LOGIN_LIST_INFO: 'hasLoginListInfo', + HAS_SUBSCRIPTION_INFO: 'hasSubscriptionInfo', + }, + DEBUG: { DETAILS: 'details', JSON: 'json', @@ -5932,6 +5988,12 @@ const CONST = { HAS_CHILD_REPORT_AWAITING_ACTION: 'hasChildReportAwaitingAction', HAS_MISSING_INVOICE_BANK_ACCOUNT: 'hasMissingInvoiceBankAccount', }, + + RBR_REASONS: { + HAS_ERRORS: 'hasErrors', + HAS_VIOLATIONS: 'hasViolations', + HAS_TRANSACTION_THREAD_VIOLATIONS: 'hasTransactionThreadViolations', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 7822ec16b879..e07b03a6d405 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -89,12 +89,12 @@ function Expensify() { const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA); const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); - const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM); - const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE); - const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED); + const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, {initWithStoredValues: false}); + const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE, {initWithStoredValues: false}); + const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED, {initWithStoredValues: false}); const [isSidebarLoaded] = useOnyx(ONYXKEYS.IS_SIDEBAR_LOADED); const [screenShareRequest] = useOnyx(ONYXKEYS.SCREEN_SHARE_REQUEST); - const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION); + const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION, {initWithStoredValues: false}); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); useEffect(() => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 14c0dc4abc50..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', @@ -441,9 +445,6 @@ const ONYXKEYS = { /** Stores recently used currencies */ RECENTLY_USED_CURRENCIES: 'nvp_recentlyUsedCurrencies', - /** States whether we transitioned from OldDot to show only certain group of screens. It should be undefined on pure NewDot. */ - IS_SINGLE_NEW_DOT_ENTRY: 'isSingleNewDotEntry', - /** Company cards custom names */ NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES: 'nvp_expensify_ccCustomNames', @@ -908,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; @@ -1007,7 +1009,6 @@ type OnyxValuesMapping = { [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; [ONYXKEYS.LAST_ROUTE]: string; - [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: boolean | undefined; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a8d562c8f244..c346da6cadcb 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -21,6 +21,7 @@ const PUBLIC_SCREENS_ROUTES = { ROOT: '', TRANSITION_BETWEEN_APPS: 'transition', CONNECTION_COMPLETE: 'connection-complete', + BANK_CONNECTION_COMPLETE: 'bank-connection-complete', VALIDATE_LOGIN: 'v/:accountID/:validateCode', UNLINK_LOGIN: 'u/:accountID/:validateCode', APPLE_SIGN_IN: 'sign-in-with-apple', @@ -147,11 +148,10 @@ const ROUTES = { }, SETTINGS_DELEGATE_CONFIRM: { route: 'settings/security/delegate/:login/role/:role/confirm', - getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm` as const, - }, - SETTINGS_DELEGATE_MAGIC_CODE: { - route: 'settings/security/delegate/:login/role/:role/magic-code', - getRoute: (login: string, role: string) => `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/magic-code` as const, + getRoute: (login: string, role: string, showValidateActionModal?: boolean) => { + const validateActionModalParam = showValidateActionModal ? `?showValidateActionModal=true` : ''; + return `settings/security/delegate/${encodeURIComponent(login)}/role/${role}/confirm${validateActionModalParam}` as const; + }, }, SETTINGS_ABOUT: 'settings/about', SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', @@ -231,7 +231,6 @@ const ROUTES = { route: 'settings/profile/contact-methods/:contactMethod/details', getRoute: (contactMethod: string, backTo?: string) => getUrlWithBackToParam(`settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, backTo), }, - SETINGS_CONTACT_METHOD_VALIDATE_ACTION: 'settings/profile/contact-methods/validate-action', SETTINGS_NEW_CONTACT_METHOD: { route: 'settings/profile/contact-methods/new', getRoute: (backTo?: string) => getUrlWithBackToParam('settings/profile/contact-methods/new', backTo), @@ -448,6 +447,59 @@ 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), + }, + SETTINGS_TAGS_SETTINGS: { + route: 'settings/:policyID/tags/settings', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/settings` as const, backTo), + }, + SETTINGS_TAGS_EDIT: { + route: 'settings/:policyID/tags/:orderWeight/edit', + getRoute: (policyID: string, orderWeight: number, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/${orderWeight}/edit` as const, backTo), + }, + SETTINGS_TAG_CREATE: { + route: 'settings/:policyID/tags/new', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/new` as const, backTo), + }, + SETTINGS_TAG_EDIT: { + route: 'settings/:policyID/tag/:orderWeight/:tagName/edit', + getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/edit` as const, backTo), + }, + SETTINGS_TAG_SETTINGS: { + route: 'settings/:policyID/tag/:orderWeight/:tagName', + getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}` as const, backTo), + }, + SETTINGS_TAG_APPROVER: { + route: 'settings/:policyID/tag/:orderWeight/:tagName/approver', + getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/approver` as const, backTo), + }, + SETTINGS_TAG_LIST_VIEW: { + route: 'settings/:policyID/tag-list/:orderWeight', + getRoute: (policyID: string, orderWeight: number, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tag-list/${orderWeight}` as const, backTo), + }, + SETTINGS_TAG_GL_CODE: { + route: 'settings/:policyID/tag/:orderWeight/:tagName/gl-code', + getRoute: (policyID: string, orderWeight: number, tagName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const, backTo), + }, + SETTINGS_TAGS_IMPORT: { + route: 'settings/:policyID/tags/import', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/import` as const, backTo), + }, + SETTINGS_TAGS_IMPORTED: { + route: 'settings/:policyID/tags/imported', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags/imported` as const, backTo), + }, SETTINGS_CATEGORIES_ROOT: { route: 'settings/:policyID/categories', getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories`, backTo), @@ -466,7 +518,25 @@ const ROUTES = { }, SETTINGS_CATEGORY_EDIT: { route: 'settings/:policyID/category/:categoryName/edit', - getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/edit`, backTo), + getRoute: (policyID: string, categoryName: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/edit`, backTo), + }, + SETTINGS_CATEGORIES_IMPORT: { + route: 'settings/:policyID/categories/import', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/import` as const, backTo), + }, + SETTINGS_CATEGORIES_IMPORTED: { + route: 'settings/:policyID/categories/imported', + getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/categories/imported` as const, backTo), + }, + SETTINGS_CATEGORY_PAYROLL_CODE: { + route: 'settings/:policyID/category/:categoryName/payroll-code', + getRoute: (policyID: string, categoryName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/payroll-code` as const, backTo), + }, + SETTINGS_CATEGORY_GL_CODE: { + route: 'settings/:policyID/category/:categoryName/gl-code', + getRoute: (policyID: string, categoryName: string, backTo = '') => + getUrlWithBackToParam(`settings/${policyID}/category/${encodeURIComponent(categoryName)}/gl-code` as const, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { route: ':action/:iouType/currency/:transactionID/:reportID/:pageIndex?', @@ -518,10 +588,6 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, orderWeight: number, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => getUrlWithBackToParam(`${action as string}/${iouType as string}/tag/${orderWeight}/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, - SETTINGS_TAGS_ROOT: { - route: 'settings/:policyID/tags', - getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo), - }, MONEY_REQUEST_STEP_WAYPOINT: { route: ':action/:iouType/waypoint/:transactionID/:reportID/:pageIndex', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID?: string, pageIndex = '', backTo = '') => @@ -962,7 +1028,7 @@ const ROUTES = { }, WORKSPACE_TAG_APPROVER: { route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/approver', - getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${tagName}/approver` as const, + getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/approver` as const, }, WORKSPACE_TAG_LIST_VIEW: { route: 'settings/workspaces/:policyID/tag-list/:orderWeight', @@ -1384,14 +1450,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 39df4594c277..2e44c5ed5695 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -74,7 +74,6 @@ const SCREENS = { DISPLAY_NAME: 'Settings_Display_Name', CONTACT_METHODS: 'Settings_ContactMethods', CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', - CONTACT_METHOD_VALIDATE_ACTION: 'Settings_ValidateContactMethodAction', NEW_CONTACT_METHOD: 'Settings_NewContactMethod', STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After', STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date', @@ -134,7 +133,6 @@ const SCREENS = { ADD_DELEGATE: 'Settings_Delegate_Add', DELEGATE_ROLE: 'Settings_Delegate_Role', DELEGATE_CONFIRM: 'Settings_Delegate_Confirm', - DELEGATE_MAGIC_CODE: 'Settings_Delegate_Magic_Code', UPDATE_DELEGATE_ROLE: 'Settings_Delegate_Update_Role', UPDATE_DELEGATE_ROLE_MAGIC_CODE: 'Settings_Delegate_Update_Magic_Code', }, @@ -177,6 +175,7 @@ const SCREENS = { SEARCH_ADVANCED_FILTERS: 'SearchAdvancedFilters', SEARCH_SAVED_SEARCH: 'SearchSavedSearch', SETTINGS_CATEGORIES: 'SettingsCategories', + SETTINGS_TAGS: 'SettingsTags', EXPENSIFY_CARD: 'ExpensifyCard', DOMAIN_CARD: 'DomainCard', RESTRICTED_ACTION: 'RestrictedAction', @@ -220,6 +219,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: { @@ -246,6 +246,10 @@ const SCREENS = { SETTINGS_CATEGORY_CREATE: 'Settings_Category_Create', SETTINGS_CATEGORY_EDIT: 'Settings_Category_Edit', SETTINGS_CATEGORIES_ROOT: 'Settings_Categories', + SETTINGS_CATEGORIES_IMPORT: 'Settings_Categories_Import', + SETTINGS_CATEGORIES_IMPORTED: 'Settings_Categories_Imported', + SETTINGS_CATEGORY_PAYROLL_CODE: 'Settings_Category_Payroll_Code', + SETTINGS_CATEGORY_GL_CODE: 'Settings_Category_GL_Code', }, EXPENSIFY_CARD: { EXPENSIFY_CARD_DETAILS: 'Expensify_Card_Details', @@ -258,7 +262,19 @@ const SCREENS = { DOMAIN_CARD_REPORT_FRAUD: 'Domain_Card_Report_Fraud', }, - SETTINGS_TAGS_ROOT: 'Settings_Tags', + SETTINGS_TAGS: { + SETTINGS_TAGS_ROOT: 'Settings_Tags', + SETTINGS_TAGS_SETTINGS: 'Settings_Tags_Settings', + SETTINGS_TAGS_EDIT: 'Settings_Tags_Edit', + SETTINGS_TAG_CREATE: 'Settings_Tag_Create', + SETTINGS_TAG_EDIT: 'Settings_Tag_Edit', + SETTINGS_TAG_SETTINGS: 'Settings_Tag_Settings', + SETTINGS_TAG_APPROVER: 'Settings_Tag_Approver', + SETTINGS_TAG_LIST_VIEW: 'Settings_Tag_List_View', + SETTINGS_TAG_GL_CODE: 'Settings_Tag_GL_Code', + SETTINGS_TAGS_IMPORT: 'Settings_Tags_Import', + SETTINGS_TAGS_IMPORTED: 'Settings_Tags_Imported', + }, REPORT_SETTINGS: { ROOT: 'Report_Settings_Root', @@ -316,6 +332,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/AttachmentOfflineIndicator.tsx b/src/components/AttachmentOfflineIndicator.tsx index d425e6f18e0e..4ff1940ba004 100644 --- a/src/components/AttachmentOfflineIndicator.tsx +++ b/src/components/AttachmentOfflineIndicator.tsx @@ -37,7 +37,7 @@ function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndica return ( @@ -431,6 +432,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/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index e63b8bb91874..fd2013c6bde7 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -137,6 +137,7 @@ function ConfirmModal({ restoreFocusType, }: ConfirmModalProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); const styles = useThemeStyles(); 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')}`} - - ) : ( -