diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 0b32d8ee6dc1..c6a6029e06e0 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -18,13 +18,13 @@ runs: desktop/package-lock.json - id: cache-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: node_modules key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json', 'patches/**') }} - id: cache-desktop-node-modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: desktop/node_modules key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json', 'desktop/patches/**') }} diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index d17760baa91f..8fe84446ba82 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -2657,12 +2657,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`) // Coercion. // Extract anything that could conceivably be a part of a valid semver -createToken('COERCE', `${'(^|[^\\d])' + +createToken('COERCEPLAIN', `${'(^|[^\\d])' + '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + - `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`) +createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`) +createToken('COERCEFULL', src[t.COERCEPLAIN] + + `(?:${src[t.PRERELEASE]})?` + + `(?:${src[t.BUILD]})?` + `(?:$|[^\\d])`) createToken('COERCERTL', src[t.COERCE], true) +createToken('COERCERTLFULL', src[t.COERCEFULL], true) // Tilde ranges. // Meaning is "reasonably at or greater than" diff --git a/.github/workflows/checkE2ETestCode.yml b/.github/workflows/checkE2ETestCode.yml new file mode 100644 index 000000000000..090b7a7f23e4 --- /dev/null +++ b/.github/workflows/checkE2ETestCode.yml @@ -0,0 +1,23 @@ +name: Check e2e test code builds correctly + +on: + workflow_call: + pull_request: + types: [opened, synchronize] + paths: + - 'tests/e2e/**' + - 'src/libs/E2E/**' + +jobs: + lint: + if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + + - name: Verify e2e tests compile correctly + run: npm run e2e-test-runner-build \ No newline at end of file diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 338cb8313465..8a47ea4bb220 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -15,6 +15,10 @@ on: type: string required: true +concurrency: + group: "${{ github.ref }}-e2e" + cancel-in-progress: true + jobs: buildBaseline: runs-on: ubuntu-latest-xl @@ -175,23 +179,11 @@ jobs: - name: Rename delta APK run: mv "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edelta-release.apk" "${{steps.downloadDeltaAPK.outputs.download-path}}/app-e2edeltaRelease.apk" - - name: Copy e2e code into zip folder - run: cp -r tests/e2e zip + - name: Compile test runner to be executable in a nodeJS environment + run: npm run e2e-test-runner-build - # Note: we can't reuse the apps tsconfig, as it depends on modules that aren't available in the AWS Device Farm environment - - name: Write tsconfig.json to zip folder - run: | - echo '{ - "compilerOptions": { - "target": "ESNext", - "module": "commonjs", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - } - }' > zip/tsconfig.json + - name: Copy e2e code into zip folder + run: cp tests/e2e/dist/index.js zip/testRunner.js - name: Zip everything in the zip directory up run: zip -qr App.zip ./zip diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 33c850823413..50e886942c98 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,8 +7,13 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', '**.json', '**.mjs', '**.cjs', 'config/.editorconfig', '.watchmanconfig', '.imgbotconfig'] +concurrency: + group: "${{ github.ref }}-lint" + cancel-in-progress: true + jobs: lint: + name: Run ESLint if: ${{ github.actor != 'OSBotify' || github.event_name == 'workflow_call' }} runs-on: ubuntu-latest steps: diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index 818441828bf0..4d6597334447 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -194,7 +194,7 @@ jobs: bundler-cache: true - name: Cache Pod dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: pods-cache with: path: ios/Pods diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bdc14950a337..71b4bc3d8fc3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,10 @@ on: branches-ignore: [staging, production] paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json'] +concurrency: + group: "${{ github.ref }}-jest" + cancel-in-progress: true + jobs: jest: if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} @@ -31,7 +35,7 @@ jobs: - name: Cache Jest cache id: cache-jest-cache - uses: actions/cache@ac25611caef967612169ab7e95533cf932c32270 + uses: actions/cache@v4 with: path: .jest-cache key: ${{ runner.os }}-jest diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 9548c3a6e595..3f02430f3c1f 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -167,7 +167,7 @@ jobs: bundler-cache: true - name: Cache Pod dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 id: pods-cache with: path: ios/Pods diff --git a/__mocks__/react-native.js b/__mocks__/react-native.ts similarity index 63% rename from __mocks__/react-native.js rename to __mocks__/react-native.ts index 1eeea877ca0f..27b78b308446 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.ts @@ -1,27 +1,47 @@ // eslint-disable-next-line no-restricted-imports import * as ReactNative from 'react-native'; -import _ from 'underscore'; +import type StartupTimer from '@libs/StartupTimer/types'; + +const {BootSplash} = ReactNative.NativeModules; jest.doMock('react-native', () => { let url = 'https://new.expensify.com/'; const getInitialURL = () => Promise.resolve(url); - let appState = 'active'; + let appState: ReactNative.AppStateStatus = 'active'; let count = 0; - const changeListeners = {}; + const changeListeners: Record void> = {}; // Tests will run with the app in a typical small screen size by default. We do this since the react-native test renderer // runs against index.native.js source and so anything that is testing a component reliant on withWindowDimensions() // would be most commonly assumed to be on a mobile phone vs. a tablet or desktop style view. This behavior can be // overridden by explicitly setting the dimensions inside a test via Dimensions.set() - let dimensions = { + let dimensions: Record = { width: 300, height: 700, scale: 1, fontScale: 1, }; - return Object.setPrototypeOf( + type ReactNativeMock = typeof ReactNative & { + NativeModules: typeof ReactNative.NativeModules & { + BootSplash: { + getVisibilityStatus: typeof BootSplash.getVisibilityStatus; + hide: typeof BootSplash.hide; + logoSizeRatio: number; + navigationBarHeight: number; + }; + StartupTimer: StartupTimer; + }; + Linking: typeof ReactNative.Linking & { + setInitialURL: (newUrl: string) => void; + }; + AppState: typeof ReactNative.AppState & { + emitCurrentTestState: (state: ReactNative.AppStateStatus) => void; + }; + }; + + const reactNativeMock: ReactNativeMock = Object.setPrototypeOf( { NativeModules: { ...ReactNative.NativeModules, @@ -36,7 +56,7 @@ jest.doMock('react-native', () => { Linking: { ...ReactNative.Linking, getInitialURL, - setInitialURL(newUrl) { + setInitialURL(newUrl: string) { url = newUrl; }, }, @@ -45,11 +65,11 @@ jest.doMock('react-native', () => { get currentState() { return appState; }, - emitCurrentTestState(state) { + emitCurrentTestState(state: ReactNative.AppStateStatus) { appState = state; - _.each(changeListeners, (listener) => listener(appState)); + Object.entries(changeListeners).forEach(([, listener]) => listener(appState)); }, - addEventListener(type, listener) { + addEventListener(type: ReactNative.AppStateEvent, listener: (state: ReactNative.AppStateStatus) => void) { if (type === 'change') { const originalCount = count; changeListeners[originalCount] = listener; @@ -68,7 +88,7 @@ jest.doMock('react-native', () => { ...ReactNative.Dimensions, addEventListener: jest.fn(), get: () => dimensions, - set: (newDimensions) => { + set: (newDimensions: Record) => { dimensions = newDimensions; }, }, @@ -78,9 +98,11 @@ jest.doMock('react-native', () => { // so it seems easier to just run the callback immediately in tests. InteractionManager: { ...ReactNative.InteractionManager, - runAfterInteractions: (callback) => callback(), + runAfterInteractions: (callback: () => void) => callback(), }, }, ReactNative, ); + + return reactNativeMock; }); diff --git a/android/app/build.gradle b/android/app/build.gradle index 0cfee206602a..e285d0bff26f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044500 - versionName "1.4.45-0" + versionCode 1001044601 + versionName "1.4.46-1" } flavorDimensions "default" diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg index 48eebf863cc3..3e2c270d681c 100644 --- a/assets/images/chatbubble-add.svg +++ b/assets/images/chatbubble-add.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/chatbubble-reply.svg b/assets/images/chatbubble-reply.svg new file mode 100644 index 000000000000..f8d53ebff807 --- /dev/null +++ b/assets/images/chatbubble-reply.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg index 492616cf2ab5..f5a27a74f5fe 100644 --- a/assets/images/chatbubble-unread.svg +++ b/assets/images/chatbubble-unread.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + diff --git a/assets/images/make-admin.svg b/assets/images/make-admin.svg new file mode 100644 index 000000000000..383708e0523c --- /dev/null +++ b/assets/images/make-admin.svg @@ -0,0 +1,12 @@ + + + + + + + diff --git a/assets/images/product-illustrations/three_legged_laptop_woman.svg b/assets/images/product-illustrations/three_legged_laptop_woman.svg new file mode 100644 index 000000000000..6be000b92e37 --- /dev/null +++ b/assets/images/product-illustrations/three_legged_laptop_woman.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/remove-members.svg b/assets/images/remove-members.svg new file mode 100644 index 000000000000..e9d7e08f5e5e --- /dev/null +++ b/assets/images/remove-members.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md "b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" similarity index 98% rename from docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md rename to "docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" index 1cf29531f696..8f6a3f1a908d 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ "b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Expensify-Visa\302\256-Commercial-Card-for-your-Company.md" @@ -1,5 +1,5 @@ --- -title: Set Up the Card for your Company +title: Set Up the Expensify Visa® Commercial Card for your Company description: Details on setting up the Expensify Card for your company as an admin --- # Overview diff --git a/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md b/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md new file mode 100644 index 000000000000..4cc95cdcf918 --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/Create-a-company-workspace.md @@ -0,0 +1,171 @@ +--- +title: Create a company workspace +description: Get started with Expensify by creating a workspace for your company +--- +
+ +# Overview + +Welcome to Expensify! If you are creating an Expensify account for your company, follow the steps below to get started. + +{% include info.html %} +You can also schedule a free private onboarding session where one of our Setup Specialists will walk you through the entire process. Check your email and notifications in Expensify for your unique signup link. +{% include end-info.html %} + +# 1. Meet Concierge + +Your personal assistant, Concierge, lives on your Expensify Home page on both desktop and the mobile app. + +Concierge will walk you through setting up your account and also provide: +- Reminders to do things like submit your expenses +- Alerts when more information is needed on an expense report +- Updates on new and improved account features + +You can also get support at any time by clicking the green chat bubble in the right corner. This will open a chat with Concierge where you can ask questions and receive direct support. + +# 2. Create a workspace + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Enter a name for the workspace (the name of your business or department is a great choice, if applicable), then click Select next to the workspace type that best fits your needs.
  6. +
+ +# 3. Add a business bank account + +Connecting your business bank account allows you to: +- Reimburse expenses via direct bank transfer +- Pay bills +- Collect invoice payments +- Issue Expensify Cards + +{% include info.html %} +The person who completes this process does not need to be a signer on the account, however they will be required to enter their own personal information as well. If someone other than the Expensify account holder enters their personal information in this section, the details will be flagged for review, and adding the bank account to Expensify will be delayed. +{% include end-info.html %} + +To add a business bank account, + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Reimbursement tab.
  8. +
  9. Click the Direct box, then click Add Business Bank Account.
  10. +
  11. Click Connect to your bank.
  12. +
  13. Click Continue to continue to Plaid.
  14. +
  15. Select the bank and log in to your business bank account.
  16. +
      +
    • If the bank is not listed, close the Plaid window and select Connect Manually to enter your account and routing numbers.
    • +
    +
  17. Select the bank account if multiple are available.
  18. +
  19. Verify the bank account by entering some additional information:
  20. +
      +
    • Enter the legal business name.
    • +
    • Enter the company address (Must be a physical location in the U.S. Maildrop address, P.O. boxes, or UPS Store addresses are flagged for review and will create a delay verifying the bank account).
    • +
    • Enter the Tax Identification Number (TIN)
    • +
    • Enter the company website, formatted like https://www.expensify.com
    • +
    • Enter the Industry Classification Code. You can locate a list of Industry Classification Codes here.
    • +
    • Enter your personal information into the Requestor Information section, including your physical U.S. address and SSN issued from the U.S.
    • +
    • Upload photos of your ID. It must be issued by the U.S. and be current (i.e., the expiration date must be in the future).
    • +
    • Take a short video of yourself to verify your identity.
    • +
    • Check the appropriate box under Additional Information to accept the agreement terms and verify that all of the information is true and accurate.
    • +
        +
      • A Beneficial Owner is an individual who owns 25% or more of the business. If you or someone else is a Beneficial Owner, check the appropriate box. If someone else is a Beneficial Owner, their personal information will need to be provided as well.
      • +
      • If no individual owns more than 25% of the company you do not need to list any beneficial owners. In that case, be sure to leave both boxes unchecked under the Beneficial Owner Additional Information section.
      • +
      +
    +
  21. Within 1-2 business days, Expensify will send three test transactions to your bank account that you’ll enter into Expensify to validate your bank account by either:
  22. +
      +
    • Clicking the validate task from Concierge on your Home page.
    • +
    • Going to Settings > Account > Payments and clicking Enter test transactions.
    • +
    +
+ +{% include info.html %} +If after two business days you do not see these test transactions, click the green chat bubble in the right corner to get support from Concierge. +{% include end-info.html %} + +# 4. Connect your accounting system + +If you use an external accounting system like QuickBooks, you can link it with Expensify to help you import accounting data, code expenses, and more. + +To add an accounting system integration, +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Connections tab.
  8. +
  9. Under Accounting Integrations, click the name of your accounting system, select the “Connect to…” option, and click the related button.
  10. +
  11. Depending on the integration you selected, you’ll either be prompted with a login screen for the accounting system or additional steps for how to proceed.
  12. +
+ +For a walkthrough for how to set up a specific accounting system, visit our [Integrations](https://help.expensify.com/expensify-classic/hubs/integrations/) articles. + +# 5. Set approval rules + +Determine the basic guidelines that apply to all submitted expenses. If a submitted expense does not meet these rules, it will be flagged as a violation. You can set rules for expenses, per diem, travel, and reports. + +
    +
  1. Hover over Settings, then click Workspaces.
  2. +
  3. Click the Group tab on the left.
  4. +
  5. Click the desired workspace name.
  6. +
  7. Click the Expenses tab and set the desired rules.
  8. +
      +
    • Determine if expense violations will be enforced. If enabled, expenses that exceed the set number of days or amount will be flagged as an expense violation.
    • +
    • Determine how cash expenses are treated.
    • +
    • Determine if expenses can be re-billed to someone else as an invoice.
    • +
    • Determine if eRecipts can be submitted as proof of an expense.
    • + +{% include info.html %} +If eReceipts are enabled, imported credit card expenses of $75 USD or less will be tracked automatically—no paper receipt is necessary. + +eReceipts meet IRS documentation requirements as per Publication 463; However, the IRS will not accept an eReceipt for lodging purchases (for example, hotel expenses will require a paper receipt). +{% include end-info.html %} + +
    • Determine if receipts are visible to anyone with the URL.
    • + +{% include info.html %} +If disabled, receipts can be seen only by admins for the workspace or someone who has been sent the report that the receipts are related to. +{% include end-info.html %} + +
    • Set your mileage rates for distance expenses.
    • +
    • Determine if time expenses can be submitted as an hourly rate.
    • +
    +
  9. Click the Reports tab and set the desired rules.
  10. +
      +
    • Set the currency that will be used for all reports.
    • +
    • Determine if Schedule Submit will be allowed. If enabled, all created expenses will be automatically assigned to a report. Concierge will then submit expenses for approval on the employee's behalf instantly either daily, weekly, etc., based on your frequency setting.
    • +
    • Determine if a default report title will be required. If enabled, all reports will be named based on the set formula.
    • +
    • Determine if additional fields should be added to each report or invoice.
    • +
    +
  11. Click the Travel tab and set the desired rules.
  12. +
      +
    • Determine what flight class and hotel rating Concierge should select when booking travel. Concierge can automatically book flights and hotels for your employees if they have an Expensify card.
    • +
    +
  13. Click the Per Diem tab and set the desired rules.
  14. +
      +
    • Determine if per diem expenses will be allowed. Then import your per diem expense rules spreadsheet that contains the list of location-based expense amounts (for example, an employee in California might receive a different amount for lunch than an employee in Louisiana).
    • +
    +
+ +# 6. Secure your account + +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication. This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. + +
    +
  1. Hover over Settings, then click Account.
  2. +
  3. Under the Account Details tab, scroll down to the Two Factor Authentication section and enable the toggle.
  4. +
  5. Save a copy of your backup codes. This step is critical—You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
  6. +
      +
    • Click Download to save a copy of your backup codes to your computer.
    • +
    • Click Copy to paste the codes into a document or other secure location.
    • +
    +
  7. Click Continue.
  8. +
  9. Download or open your authenticator app and either:
  10. +
      +
    • Scan the QR code shown on your computer screen.
    • +
    • Enter the 6-digit code from your authenticator app into Expensify and click Verify.
    • +
    +
+ +When you log in to Expensify in the future, you’ll be emailed a magic code that you’ll use to log in with. Then you’ll be prompted to open your authenticator app to get the 6-digit code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. diff --git a/docs/articles/expensify-classic/settings/Account-Details.md b/docs/articles/expensify-classic/settings/Account-Details.md deleted file mode 100644 index 535e74eeb701..000000000000 --- a/docs/articles/expensify-classic/settings/Account-Details.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Account Details -description: The Account Details section of your account is where you can update your profile photo, enable 2FA, and change the email address associated with your Expensify account. ---- - -# Overview -The Account Details section of your account is where you can update your profile photo, enable 2FA, and change the email address associated with your account. - -You can have multiple email addresses tied to your account to make it easier to submit expenses or manage your account. Let’s go over how to configure the various account settings located under the Account Details section of your Expensify account. - -# How to add a profile photo -To update your name or add a profile photo, navigate to **Settings** > **Account** > **Account Details.** Under “your profile” you’ll notice a profile picture thumbnail, click “edit photo” underneath to update the profile image. - -# How to enable Two-Factor Authentication -Setting up Two-factor Authentication is one of the best ways to secure your account. This can be enabled individually in your account settings by following **Settings** > **Accounts** > **Account Details** > **Two Factor Authentication** and toggle the switch to **Enabled.** - -Save or download your **Recovery Codes.** It’s important to keep these safe! You WILL lose access to your account if you cannot use your authenticator app and do not have your recovery codes. - -Use your favorite authenticator app to connect to Expensify using the QR code or click the link to enter the secret key manually. - -Once connected, quickly enter the code generated by your app into Expensify before the timeframe runs out! - -# How to manage your devices -You can access your Expensify account on multiple devices, which allows for easy access to your account data. By heading to **Settings** > **Account** > **Account Details** > **Device Management**, you can review the devices that have access to your account. - -From that same place in your account, you can remove any devices that should no longer have access. To do this, select the **Revoke** button next to each device you wish to remove access to your account. - -# How to add a Secondary Login -A Secondary Login is helpful if you have multiple email addresses and don’t necessarily need multiple Expensify accounts. By adding additional emails to your Expensify account, you can use them to forward receipts to receipts@expensify.com and they will be uploaded to your main Expensify account. To get this added to your account, follow these steps: - -1. Log in to your Expensify account through a web browser at www.expensify.com. Please note that this process cannot be completed using the mobile app; it must be done from the website at expensify.com. -2. Navigate to **Settings** > **Account** > **Account Details**. Scroll down to find the 'Secondary Logins' section, then click the 'Add Secondary Login' button. -3. Input the email address or mobile phone number you wish to add, ensuring you include the international code if applicable. -4. You will receive a prompt to enter the Magic Code, which will be sent to the email address you're adding as a secondary login. - -# How to update your email address -Once a Secondary Login is added to your account, you can make it your primary email address. The primary address on an Expensify account is the address that will receive email notifications and updates regarding the account. Any new email addresses must be added as a secondary login before they can be made a primary address. - -1. Log in to your Expensify account through a web browser at www.expensify.com. Please note that this process cannot be completed using the mobile app; it must be done from the website at expensify.com. -2. Navigate to **Settings** > **Account** > **Account Details**. Scroll down to find the 'Secondary Logins' section, then select the **"Make Primary"** button next to the email address. -3. You can keep the old address as a secondary login or delete email addresses by selecting the **"Remove"** button. - - -# Deep Dive -## Managing emails connected to other Expensify accounts -A secondary login can only be added if it is not linked to an existing account. If you have two email addresses with Expensify accounts linked to them, you'll need to merge them instead. - -Alternatively, you can remove a personal email address from a previous work/organization account to use it elsewhere. - -Is your Secondary Login (personal email) validated in your company account? If so, do the following: -1. Navigate to expensify.com -2. Log in using your validated Secondary Login -3. Navigate to **Account** > **Settings** > **Account Details** > **Secondary Logins** -4. Remove your personal email address from the account by clicking the **"Remove"** button next to your email - -Is your Secondary Login (personal email) invalidated in your company account? If so, do the following: -1. Navigate to expensify.com -2. Enter your invalidated secondary login email address -3. You will be presented with a confirmation message saying Expensify sent you an email with a validation link -4. Head to your personal email account and follow the prompts -5. You'll receive a link in the email to click that will unlink the two accounts - -{% include faq-begin.md %} -## The profile picture on my account updated automatically. Why did this happen? -Our focus is always on making your experience user-friendly and saving you valuable time. One of the ways we achieve this is by utilizing a public API to retrieve public data linked to your email address. - -This tool searches for public accounts or profiles associated with your email address, such as on LinkedIn. When it identifies one, it pulls in the uploaded profile picture and name to Expensify. - -While this automated process is generally accurate, there may be instances where it's not entirely correct. If this happens, we apologize for any inconvenience caused. The good news is that rectifying such situations is a straightforward process. You can quickly update your information manually by following the directions provided above, ensuring your data is accurate and up to date in no time. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/settings/Merge-Accounts.md b/docs/articles/expensify-classic/settings/Merge-Accounts.md deleted file mode 100644 index 34bf422aa983..000000000000 --- a/docs/articles/expensify-classic/settings/Merge-Accounts.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Merge Accounts -description: How to merge two Expensify accounts and why this is useful. ---- - -# Overview - -Merging accounts allows you to combine two accounts. When you combine two accounts, all receipts, expenses, expense reports, invoices, bills, imported cards, secondary logins, co-pilots, and group workspace settings will be combined into one account. -This can be useful if you start off with an account of your own but your organization creates a separate account for you. You can then track both personal and business expenses via one account. - -# How to merge accounts -Merging two accounts together is fairly straightforward. Let’s go over how to do that below: -1. Navigate to [expensify.com](https://www.expensify.com) -2. Log into the account you want to set as the Primary account -3. Navigate to **Settings > Account > Account Details** -4. Scroll down to Merge Accounts and fill in the fields -6. Click Merge Accounts -7. Once you click Merge, a magic code is sent to you via email -8. Paste the code into the required field -If you have any questions about this process, feel free to reach out to Concierge for some assistance! - -{% include faq-begin.md %} -## Can you merge accounts from the mobile app? -No, accounts can only be merged from the full website at expensify.com. -## Can I administratively merge two accounts together? -No, only the account holder (member) can perform account merging. -## Is merging accounts reversible? -No, merging accounts is not reversible. It is a permanent action that cannot be undone. -## I have open expenses in the account I'm merging from. Will those expenses merge into the new account? -All expenses must be reported and submitted for them to merge into the new account. Any open expenses will not merge. -## Are there any restrictions on account merging? -Yes! Please see below: -- If your email address belongs to a verified domain (verified in Expensify), you must start the process from the email account under the verified domain. You cannot merge a verified company email account into a personal account. -- If you have two accounts with two different verified domains, you cannot merge them together. -## What happens to my “personal” Individual workspace when merging accounts? -The old “personal” Individual workspace is deleted. If you plan to submit reports under a different workspace in the future, ensure that any reports on the Individual workspace in the old account are marked as Open before merging the accounts. You can typically do this by selecting “Undo Submit” on any submitted reports. - -{% include faq-end.md %} diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 9d5fb4c58a75..374594698200 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.45 + 1.4.46 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.45.0 + 1.4.46.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 40a633aa93d3..e314ea1595e1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.45 + 1.4.46 CFBundleSignature ???? CFBundleVersion - 1.4.45.0 + 1.4.46.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 69058d2349dc..aaec6344175f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.45 + 1.4.46 CFBundleVersion - 1.4.45.0 + 1.4.46.1 NSExtension NSExtensionPointIdentifier diff --git a/jest.config.js b/jest.config.js index 441507af4228..5b36e44c7581 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,7 +23,7 @@ module.exports = { }, testEnvironment: 'jsdom', setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect', '/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], + setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.ts', diff --git a/jest/setupAfterEnv.ts b/jest/setupAfterEnv.ts index 6f7836b64dbb..d59495874588 100644 --- a/jest/setupAfterEnv.ts +++ b/jest/setupAfterEnv.ts @@ -1 +1,4 @@ +// This is required in order for jest to recognize custom matchers like toBeDisabled. This can be removed once testing-library/react-native version is bumped to v12.4 or later +import '@testing-library/jest-native/extend-expect'; + jest.useRealTimers(); diff --git a/package-lock.json b/package-lock.json index 4823fb44b7ff..44cabdeb2cb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.45-0", + "version": "1.4.46-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.45-0", + "version": "1.4.46-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -37,7 +37,7 @@ "@react-native-google-signin/google-signin": "^10.0.1", "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", - "@react-navigation/native": "6.1.8", + "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -52,7 +52,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -97,7 +97,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.7", + "react-native-onyx": "2.0.10", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -202,7 +202,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^26.6.8", + "electron": "^29.0.0", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -10317,9 +10317,9 @@ } }, "node_modules/@react-navigation/core": { - "version": "6.4.10", - "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.10.tgz", - "integrity": "sha512-oYhqxETRHNHKsipm/BtGL0LI43Hs2VSFoWMbBdHK9OqgQPjTVUitslgLcPpo4zApCcmBWoOLX2qPxhsBda644A==", + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.11.tgz", + "integrity": "sha512-kOCyOc1L0lAl53DbyNl3OkUJwSFKSaVCsV8leJawUXMXJ1FTT3nbS3xMOqbZuchxIbl8T62sZ7YnlWG/21rcMw==", "dependencies": { "@react-navigation/routers": "^6.1.9", "escape-string-regexp": "^4.0.0", @@ -10345,6 +10345,17 @@ "react": "*" } }, + "node_modules/@react-navigation/elements": { + "version": "1.3.24", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.24.tgz", + "integrity": "sha512-zgZV50qjlv3/N+MzNV0DIRmtg30IZcR0+LaTQRP/OxLtveQkgUG6wIEKl6SXO2ykC9yF9V82msdCzKl9uPSQCA==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-navigation/material-top-tabs": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz", @@ -10362,11 +10373,11 @@ } }, "node_modules/@react-navigation/native": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.8.tgz", - "integrity": "sha512-0alti852nV+8oCVm9H80G6kZvrHoy51+rXBvVCRUs2rNDDozC/xPZs8tyeCJkqdw3cpxZDK8ndXF22uWq28+0Q==", + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.12.tgz", + "integrity": "sha512-t6y7sDCr0HlMf+5TuVjLjyi0ySs0eNGfreDKcWOMEi5wooNFM4LhcUCdEVylpwCPfjQMW/lNVomNromqZFM6HQ==", "dependencies": { - "@react-navigation/core": "^6.4.9", + "@react-navigation/core": "^6.4.11", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.1.23" @@ -10402,17 +10413,6 @@ "react-native-screens": ">= 3.0.0" } }, - "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", - "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0" - } - }, "node_modules/@react-ng/bounds-observer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", @@ -24894,20 +24894,22 @@ } }, "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "license": "ISC", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.2.tgz", + "integrity": "sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg==", "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", "create-hash": "^1.2.0", "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", + "elliptic": "^6.5.4", "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" + "parse-asn1": "^5.1.6", + "readable-stream": "^3.6.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 4" } }, "node_modules/browserify-sign/node_modules/readable-stream": { @@ -25920,9 +25922,9 @@ } }, "node_modules/classnames": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz", - "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==" + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", + "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -28644,14 +28646,14 @@ } }, "node_modules/electron": { - "version": "26.6.8", - "resolved": "https://registry.npmjs.org/electron/-/electron-26.6.8.tgz", - "integrity": "sha512-nuzJ5nVButL1jErc97IVb+A6jbContMg5Uuz5fhmZ4NLcygLkSW8FZpnOT7A4k8Saa95xDJOvqGZyQdI/OPNFw==", + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-29.0.0.tgz", + "integrity": "sha512-HhrRC5vWb6fAbWXP3A6ABwKUO9JvYSC4E141RzWFgnDBqNiNtabfmgC8hsVeCR65RQA2MLSDgC8uP52I9zFllQ==", "dev": true, "hasInstallScript": true, "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^18.11.18", + "@types/node": "^20.9.0", "extract-zip": "^2.0.1" }, "bin": { @@ -28932,15 +28934,6 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.435.tgz", "integrity": "sha512-B0CBWVFhvoQCW/XtjRzgrmqcgVWg6RXOEM/dK59+wFV93BFGR6AeNKc4OyhM+T3IhJaOOG8o/V+33Y2mwJWtzw==" }, - "node_modules/electron/node_modules/@types/node": { - "version": "18.19.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz", - "integrity": "sha512-g1pZtPhsvGVTwmeVoexWZLTQaOvXwoSq//pTL0DHeNzUDrFnir4fgETdhjhIxjVnN+hKOuh98+E1eMLnUXstFg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/element-resize-detector": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", @@ -30758,11 +30751,11 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", - "integrity": "sha512-nAe0fPbfRn/VYHe6mCp/APmMbda/NiHE3aZq7q0kWhPmz1LVTukeaREmZ7SN8auyLOy9/mS0RIQLeV0AR8vsrA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", "license": "MIT", "dependencies": { - "classnames": "2.4.0", + "classnames": "2.5.0", "clipboard": "2.0.11", "html-entities": "^2.4.0", "jquery": "3.6.0", @@ -30771,7 +30764,7 @@ "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.5.2", + "semver": "^7.6.0", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "ua-parser-js": "^1.0.37", "underscore": "1.13.6" @@ -44490,9 +44483,9 @@ } }, "node_modules/react-native-onyx": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.7.tgz", - "integrity": "sha512-UGMUTSFxYEzNn3wuCGzaf0t6D5XwcE+3J2pYj7wPlbskdcHVLijZZEwgSSDBF7hgNfCuZ+ImetskPNktnf9hkg==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-2.0.10.tgz", + "integrity": "sha512-XHJdKBZnUyoRKrBgZlv/p6ehuFvqXqwqQlapmVwwIU40KQQes58gPy+8HnRndT3CdAeElVWZnw/BUMtiD/F3Xw==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -47000,9 +46993,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, diff --git a/package.json b/package.json index d75ecb9ad3e1..5ce77f47aedc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.45-0", + "version": "1.4.46-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.", @@ -55,7 +55,8 @@ "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", - "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1" + "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", + "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", @@ -85,7 +86,7 @@ "@react-native-google-signin/google-signin": "^10.0.1", "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", - "@react-navigation/native": "6.1.8", + "@react-navigation/native": "6.1.12", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -100,7 +101,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#83ae6194b3e4feb363ea9d061085a7ab76e35ffb", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -145,7 +146,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "2.0.7", + "react-native-onyx": "2.0.10", "react-native-pager-view": "6.2.2", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", @@ -250,7 +251,7 @@ "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", "dotenv": "^16.0.3", - "electron": "^26.6.8", + "electron": "^29.0.0", "electron-builder": "24.6.4", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", diff --git a/patches/@react-navigation+native+6.1.8.patch b/patches/@react-navigation+native+6.1.12.patch similarity index 97% rename from patches/@react-navigation+native+6.1.8.patch rename to patches/@react-navigation+native+6.1.12.patch index c461d7e510fe..d451d89d687c 100644 --- a/patches/@react-navigation+native+6.1.8.patch +++ b/patches/@react-navigation+native+6.1.12.patch @@ -133,7 +133,7 @@ index 0000000..16da117 +//# sourceMappingURL=findFocusedRouteKey.js.map \ No newline at end of file diff --git a/node_modules/@react-navigation/native/lib/module/useLinking.js b/node_modules/@react-navigation/native/lib/module/useLinking.js -index 6f0ac51..a77b608 100644 +index 6688c62..95a0e32 100644 --- a/node_modules/@react-navigation/native/lib/module/useLinking.js +++ b/node_modules/@react-navigation/native/lib/module/useLinking.js @@ -2,6 +2,7 @@ import { findFocusedRoute, getActionFromState as getActionFromStateDefault, getP @@ -189,17 +189,17 @@ index 6f0ac51..a77b608 100644 export default function useLinking(ref, _ref) { let { independent, -@@ -231,6 +270,9 @@ export default function useLinking(ref, _ref) { +@@ -234,6 +273,9 @@ export default function useLinking(ref, _ref) { // Otherwise it's likely a change triggered by `popstate` path !== pendingPath) { const historyDelta = (focusedState.history ? focusedState.history.length : focusedState.routes.length) - (previousFocusedState.history ? previousFocusedState.history.length : previousFocusedState.routes.length); -+ ++ + // The historyDelta and historyDeltaByKeys may differ if the new state has an entry that didn't exist in previous state + const historyDeltaByKeys = getHistoryDeltaByKeys(focusedState, previousFocusedState); if (historyDelta > 0) { // If history length is increased, we should pushState // Note that path might not actually change here, for example, drawer open should pushState -@@ -242,34 +284,55 @@ export default function useLinking(ref, _ref) { +@@ -245,7 +287,8 @@ export default function useLinking(ref, _ref) { // If history length is decreased, i.e. entries were removed, we want to go back const nextIndex = history.backIndex({ @@ -209,7 +209,8 @@ index 6f0ac51..a77b608 100644 }); const currentIndex = history.index; try { - if (nextIndex !== -1 && nextIndex < currentIndex) { +@@ -254,27 +297,47 @@ export default function useLinking(ref, _ref) { + history.get(nextIndex - currentIndex)) { // An existing entry for this path exists and it's less than current index, go back to that await history.go(nextIndex - currentIndex); + history.replace({ @@ -263,7 +264,7 @@ index 6f0ac51..a77b608 100644 + path, + state + }); -+ } ++ } } } else { // If no common navigation state was found, assume it's a replace diff --git a/src/App.tsx b/src/App.tsx index cbe5948f8d4e..0e247d5faa53 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -83,7 +83,6 @@ function App({url}: AppProps) { - {/* @ts-expect-error TODO: Remove this once Expensify (https://github.com/Expensify/App/issues/25231) is migrated to TypeScript. */} diff --git a/src/CONST.ts b/src/CONST.ts index cf0b54bceb42..9ed2903941b6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -308,6 +308,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission', }, BUTTON_STATES: { DEFAULT: 'default', @@ -690,6 +691,7 @@ const CONST = { DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -1387,6 +1389,11 @@ const CONST = { }, ID_FAKE: '_FAKE_', EMPTY: 'EMPTY', + MEMBERS_BULK_ACTION_TYPES: { + REMOVE: 'remove', + MAKE_MEMBER: 'makeMember', + MAKE_ADMIN: 'makeAdmin', + }, }, CUSTOM_UNITS: { @@ -1543,6 +1550,8 @@ const CONST = { PATH_WITHOUT_POLICY_ID: /\/w\/[a-zA-Z0-9]+(\/|$)/, POLICY_ID_FROM_PATH: /\/w\/([a-zA-Z0-9]+)(\/|$)/, + + SHORT_MENTION: new RegExp(`@[\\w\\-\\+\\'#]+(?:\\.[\\w\\-\\'\\+]+)*`, 'gim'), }, PRONOUNS: { @@ -3277,6 +3286,12 @@ const CONST = { REPORT_FIELD_TITLE_FIELD_ID: 'text_title', + /** Dimensions for illustration shown in Confirmation Modal */ + CONFIRM_CONTENT_SVG_SIZE: { + HEIGHT: 220, + WIDTH: 130, + }, + DEBUG_CONSOLE: { LEVELS: { INFO: 'INFO', @@ -3320,6 +3335,14 @@ const CONST = { PREFER_CLASSIC: 'preferClassic', }, }, + + SESSION_STORAGE_KEYS: { + INITIAL_URL: 'INITIAL_URL', + }, + + AUTH_TOKEN_TYPE: { + ANONYMOUS: 'anonymousAccount', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/Expensify.js b/src/Expensify.tsx similarity index 64% rename from src/Expensify.js rename to src/Expensify.tsx index dfb59a0f8848..f822862ec434 100644 --- a/src/Expensify.js +++ b/src/Expensify.tsx @@ -1,9 +1,8 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import type {NativeEventSubscription} from 'react-native'; import {AppState, Linking} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx, {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import ConfirmModal from './components/ConfirmModal'; import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; @@ -12,14 +11,13 @@ import GrowlNotification from './components/GrowlNotification'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import UpdateAppModal from './components/UpdateAppModal'; -import withLocalize, {withLocalizePropTypes} from './components/withLocalize'; import CONST from './CONST'; +import useLocalize from './hooks/useLocalize'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; import * as ActiveClientManager from './libs/ActiveClientManager'; import BootSplash from './libs/BootSplash'; -import compose from './libs/compose'; import * as Growl from './libs/Growl'; import Log from './libs/Log'; import migrateOnyx from './libs/migrateOnyx'; @@ -27,16 +25,18 @@ import Navigation from './libs/Navigation/Navigation'; import NavigationRoot from './libs/Navigation/NavigationRoot'; import NetworkConnection from './libs/NetworkConnection'; import PushNotification from './libs/Notification/PushNotification'; -// eslint-disable-next-line no-unused-vars -import subscribePushNotification from './libs/Notification/PushNotification/subscribePushNotification'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import './libs/Notification/PushNotification/subscribePushNotification'; import StartupTimer from './libs/StartupTimer'; // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection -// eslint-disable-next-line no-unused-vars +// eslint-disable-next-line @typescript-eslint/no-unused-vars import UnreadIndicatorUpdater from './libs/UnreadIndicatorUpdater'; import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; +import type {Route} from './ROUTES'; +import type {ScreenShareRequest, Session} from './types/onyx'; Onyx.registerLogger(({level, message}) => { if (level === 'alert') { @@ -47,82 +47,63 @@ Onyx.registerLogger(({level, message}) => { } }); -const propTypes = { - /* Onyx Props */ +type ExpensifyOnyxProps = { + /** Whether the app is waiting for the server's response to determine if a room is public */ + isCheckingPublicRoom: OnyxEntry; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), + session: OnyxEntry; /** Whether a new update is available and ready to install. */ - updateAvailable: PropTypes.bool, + updateAvailable: OnyxEntry; - /** Tells us if the sidebar has rendered - TODO: We don't use it as temporary solution to fix not hidding splashscreen */ - // eslint-disable-next-line react/no-unused-prop-types - isSidebarLoaded: PropTypes.bool, + /** Tells us if the sidebar has rendered */ + isSidebarLoaded: OnyxEntry; /** Information about a screen share call requested by a GuidesPlus agent */ - screenShareRequest: PropTypes.shape({ - /** Access token required to join a screen share room, generated by the backend */ - accessToken: PropTypes.string, - - /** Name of the screen share room to join */ - roomName: PropTypes.string, - }), - - /** Whether the app is waiting for the server's response to determine if a room is public */ - isCheckingPublicRoom: PropTypes.bool, + screenShareRequest: OnyxEntry; /** True when the user must update to the latest minimum version of the app */ - updateRequired: PropTypes.bool, + updateRequired: OnyxEntry; /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ - focusModeNotification: PropTypes.bool, + focusModeNotification: OnyxEntry; /** Last visited path in the app */ - lastVisitedPath: PropTypes.string, - - ...withLocalizePropTypes, + lastVisitedPath: OnyxEntry; }; -const defaultProps = { - session: { - authToken: null, - accountID: null, - }, - updateAvailable: false, - isSidebarLoaded: false, - screenShareRequest: null, - isCheckingPublicRoom: true, - updateRequired: false, - focusModeNotification: false, - lastVisitedPath: undefined, -}; +type ExpensifyProps = ExpensifyOnyxProps; const SplashScreenHiddenContext = React.createContext({}); -function Expensify(props) { - const appStateChangeListener = useRef(null); +function Expensify({ + isCheckingPublicRoom = true, + session, + updateAvailable, + isSidebarLoaded = false, + screenShareRequest, + updateRequired = false, + focusModeNotification = false, + lastVisitedPath, +}: ExpensifyProps) { + const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); const [isSplashHidden, setIsSplashHidden] = useState(false); const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false); - const [initialUrl, setInitialUrl] = useState(null); + const {translate} = useLocalize(); + const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { - if (props.isCheckingPublicRoom) { + if (isCheckingPublicRoom) { return; } setAttemptedToOpenPublicRoom(true); - }, [props.isCheckingPublicRoom]); + }, [isCheckingPublicRoom]); - const isAuthenticated = useMemo(() => Boolean(lodashGet(props.session, 'authToken', null)), [props.session]); - const autoAuthState = useMemo(() => lodashGet(props.session, 'autoAuthState', ''), [props.session]); + const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]); + const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]); const contextValue = useMemo( () => ({ @@ -168,8 +149,16 @@ function Expensify(props) { Log.info('[BootSplash] splash screen status', false, {appState, status}); if (status === 'visible') { - const propsToLog = _.omit(props, ['children', 'session']); - propsToLog.isAuthenticated = isAuthenticated; + const propsToLog: Omit = { + isCheckingPublicRoom, + updateRequired, + updateAvailable, + isSidebarLoaded, + screenShareRequest, + focusModeNotification, + isAuthenticated, + lastVisitedPath, + }; Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false); } }); @@ -194,7 +183,7 @@ function Expensify(props) { // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report Linking.getInitialURL().then((url) => { setInitialUrl(url); - Report.openReportFromDeepLink(url, isAuthenticated); + Report.openReportFromDeepLink(url ?? '', isAuthenticated); }); // Open chat report from a deep link (only mobile native) @@ -216,7 +205,7 @@ function Expensify(props) { return null; } - if (props.updateRequired) { + if (updateRequired) { throw new Error(CONST.ERROR.UPDATE_REQUIRED); } @@ -231,20 +220,19 @@ function Expensify(props) { {/* We include the modal for showing a new update at the top level so the option is always present. */} - {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */} - {props.updateAvailable && !props.updateRequired ? : null} - {props.screenShareRequest ? ( + {updateAvailable && !updateRequired ? : null} + {screenShareRequest ? ( User.joinScreenShare(props.screenShareRequest.accessToken, props.screenShareRequest.roomName)} + title={translate('guides.screenShare')} + onConfirm={() => User.joinScreenShare(screenShareRequest.accessToken, screenShareRequest.roomName)} onCancel={User.clearScreenShareRequest} - prompt={props.translate('guides.screenShareRequest')} - confirmText={props.translate('common.join')} - cancelText={props.translate('common.decline')} + prompt={translate('guides.screenShareRequest')} + confirmText={translate('common.join')} + cancelText={translate('common.decline')} isVisible /> ) : null} - {props.focusModeNotification ? : null} + {focusModeNotification ? : null} )} @@ -254,7 +242,7 @@ function Expensify(props) { @@ -265,40 +253,35 @@ function Expensify(props) { ); } -Expensify.propTypes = propTypes; -Expensify.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - isCheckingPublicRoom: { - key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, - initWithStoredValues: false, - }, - session: { - key: ONYXKEYS.SESSION, - }, - updateAvailable: { - key: ONYXKEYS.UPDATE_AVAILABLE, - initWithStoredValues: false, - }, - isSidebarLoaded: { - key: ONYXKEYS.IS_SIDEBAR_LOADED, - }, - screenShareRequest: { - key: ONYXKEYS.SCREEN_SHARE_REQUEST, - }, - updateRequired: { - key: ONYXKEYS.UPDATE_REQUIRED, - initWithStoredValues: false, - }, - focusModeNotification: { - key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, - initWithStoredValues: false, - }, - lastVisitedPath: { - key: ONYXKEYS.LAST_VISITED_PATH, - }, - }), -)(Expensify); +export default withOnyx({ + isCheckingPublicRoom: { + key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, + initWithStoredValues: false, + }, + session: { + key: ONYXKEYS.SESSION, + }, + updateAvailable: { + key: ONYXKEYS.UPDATE_AVAILABLE, + initWithStoredValues: false, + }, + updateRequired: { + key: ONYXKEYS.UPDATE_REQUIRED, + initWithStoredValues: false, + }, + isSidebarLoaded: { + key: ONYXKEYS.IS_SIDEBAR_LOADED, + }, + screenShareRequest: { + key: ONYXKEYS.SCREEN_SHARE_REQUEST, + }, + focusModeNotification: { + key: ONYXKEYS.FOCUS_MODE_NOTIFICATION, + initWithStoredValues: false, + }, + lastVisitedPath: { + key: ONYXKEYS.LAST_VISITED_PATH, + }, +})(Expensify); export {SplashScreenHiddenContext}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 22ebffd52eec..e9cdce4f6ed9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -486,6 +486,18 @@ const ROUTES = { route: 'workspace/:policyID/workflows', getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, }, + WORKSPACE_WORKFLOWS_APPROVER: { + route: 'workspace/:policyID/settings/workflows/approver', + getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const, + }, + WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY: { + route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency', + getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency` as const, + }, + WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET: { + route: 'workspace/:policyID/settings/workflows/auto-reporting-frequency/monthly-offset', + getRoute: (policyID: string) => `workspace/${policyID}/settings/workflows/auto-reporting-frequency/monthly-offset` as const, + }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card` as const, @@ -526,6 +538,10 @@ const ROUTES = { route: 'workspace/:policyID/categories', getRoute: (policyID: string) => `workspace/${policyID}/categories` as const, }, + WORKSPACE_CATEGORY_SETTINGS: { + route: 'workspace/:policyID/categories/:categoryName', + getRoute: (policyID: string, categoryName: string) => `workspace/${policyID}/categories/${encodeURI(categoryName)}` as const, + }, WORKSPACE_CATEGORIES_SETTINGS: { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ac75968e68b9..ff3dbfd7f901 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -216,9 +216,13 @@ const SCREENS = { CATEGORIES: 'Workspace_Categories', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', + WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', + WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', }, diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 4f21f4aa1dc3..39c91c2a0789 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -350,7 +350,7 @@ function AddressSearch( return ( {!!title && {title}} - {subtitle} + {subtitle} ); }} diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.js index b2c9fed64467..e924cb8c13e9 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.js @@ -109,6 +109,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}) { isHovered={isModalHovered} isFocused={isFocused} optionalVideoDuration={item.duration} + isUsedInCarousel /> diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.js index 56425f64a51c..f6a56dc73088 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.js @@ -160,8 +160,9 @@ function AttachmentView({ const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; const onPDFLoadComplete = (path) => { - if (path && (transaction.transactionID || reportActionID)) { - CachedPDFPaths.add(transaction.transactionID || reportActionID, path); + const id = (transaction && transaction.transactionID) || reportActionID; + if (path && id) { + CachedPDFPaths.add(id, path); } if (!loadComplete) { setLoadComplete(true); diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 8ea8a1bb6f64..2b2d0a60f657 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -19,7 +19,7 @@ type AvatarProps = { source?: AvatarSource; /** Extra styles to pass to Image */ - imageStyles?: StyleProp; + imageStyles?: StyleProp; /** Additional styles to pass to Icon */ iconAdditionalStyles?: StyleProp; @@ -81,7 +81,7 @@ function Avatar({ const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill; @@ -92,7 +92,15 @@ function Avatar({ return ( - {typeof avatarSource === 'function' || typeof avatarSource === 'number' ? ( + {typeof avatarSource === 'string' ? ( + + setImageError(true)} + /> + + ) : ( - ) : ( - - setImageError(true)} - /> - )} ); diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 4388ebb8f815..5755c69641c8 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -103,7 +103,7 @@ type AvatarWithImagePickerProps = { isFocused: boolean; /** Style applied to the avatar */ - avatarStyle: StyleProp; + avatarStyle: StyleProp; /** Indicates if picker feature should be disabled */ disabled?: boolean; @@ -279,8 +279,6 @@ function AvatarWithImagePicker({ vertical: y + height + variables.spacing2, }); }); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuVisible, windowWidth]); return ( diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 1777b239e714..a25c7ff7129c 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -315,7 +315,7 @@ function Button( large ? styles.buttonLarge : undefined, success ? styles.buttonSuccess : undefined, danger ? styles.buttonDanger : undefined, - isDisabled && (success || danger) ? styles.buttonOpacityDisabled : undefined, + isDisabled ? styles.buttonOpacityDisabled : undefined, isDisabled && !danger && !success ? styles.buttonDisabled : undefined, shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined, shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined, diff --git a/src/components/ButtonWithDropdownMenu.tsx b/src/components/ButtonWithDropdownMenu/index.tsx similarity index 67% rename from src/components/ButtonWithDropdownMenu.tsx rename to src/components/ButtonWithDropdownMenu/index.tsx index 8aa3a5f0b9f0..61d3409c65ab 100644 --- a/src/components/ButtonWithDropdownMenu.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,74 +1,26 @@ -import type {RefObject} from 'react'; import React, {useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import PopoverMenu from '@components/PopoverMenu'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import type {AnchorPosition} from '@styles/index'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; -import type IconAsset from '@src/types/utils/IconAsset'; -import Button from './Button'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import PopoverMenu from './PopoverMenu'; +import type {AnchorPosition} from '@src/styles'; +import type {ButtonWithDropdownMenuProps} from './types'; -type DropdownOption = { - value: T; - text: string; - icon?: IconAsset; - iconWidth?: number; - iconHeight?: number; - iconDescription?: string; -}; - -type ButtonWithDropdownMenuProps = { - /** Text to display for the menu header */ - menuHeaderText?: string; - - /** Callback to execute when the main button is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent | undefined, value: T) => void; - - /** Callback to execute when a dropdown option is selected */ - onOptionSelected?: (option: DropdownOption) => void; - - /** Call the onPress function on main button when Enter key is pressed */ - pressOnEnter?: boolean; - - /** Whether we should show a loading state for the main button */ - isLoading?: boolean; - - /** The size of button size */ - buttonSize: ValueOf; - - /** Should the confirmation button be disabled? */ - isDisabled?: boolean; - - /** Additional styles to add to the component */ - style?: StyleProp; - - /** Menu options to display */ - /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ - options: Array>; - - /** The anchor alignment of the popover menu */ - anchorAlignment?: AnchorAlignment; - - /* ref for the button */ - buttonRef?: RefObject; - - /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ - enterKeyEventListenerPriority?: number; -}; - -function ButtonWithDropdownMenu({ +function ButtonWithDropdownMenu({ + success = false, isLoading = false, isDisabled = false, pressOnEnter = false, + shouldAlwaysShowDropdownMenu = false, menuHeaderText = '', + customText, style, buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM, anchorAlignment = { @@ -80,7 +32,7 @@ function ButtonWithDropdownMenu({ options, onOptionSelected, enterKeyEventListenerPriority = 0, -}: ButtonWithDropdownMenuProps) { +}: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -115,27 +67,27 @@ function ButtonWithDropdownMenu({ return ( - {options.length > 1 ? ( + {shouldAlwaysShowDropdownMenu || options.length > 1 ? (