diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index 93ea47bed2ae..c8360931845a 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -1928,7 +1928,7 @@ class SemVer { do { const a = this.build[i] const b = other.build[i] - debug('prerelease compare', i, a, b) + debug('build compare', i, a, b) if (a === undefined && b === undefined) { return 0 } else if (b === undefined) { diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 624c00de6831..5030ea1c2f2b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,6 +28,24 @@ jobs: - name: 🚀 Push tags to trigger staging deploy 🚀 run: git push --tags + + - name: Warn deployers if staging deploy failed + if: ${{ failure() }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 NewDot staging deploy failed. 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} deployProduction: runs-on: ubuntu-latest @@ -65,6 +83,24 @@ jobs: PR_LIST: ${{ steps.getReleasePRList.outputs.PR_LIST }} - name: 🚀 Create release to trigger production deploy 🚀 - run: gh release create ${{ env.PRODUCTION_VERSION }} --generate-notes + run: gh release create ${{ env.PRODUCTION_VERSION }} --notes '${{ steps.getReleaseBody.outputs.RELEASE_BODY }}' env: GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }} + + - name: Warn deployers if production deploy failed + if: ${{ failure() }} + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + channel: '#deployer', + attachments: [{ + color: "#DB4545", + pretext: ``, + text: `💥 NewDot production deploy failed. 💥`, + }] + } + env: + GITHUB_TOKEN: ${{ github.token }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} diff --git a/android/app/build.gradle b/android/app/build.gradle index 4f1a95ed2786..c53ad2cd3cc7 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -107,8 +107,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009000505 - versionName "9.0.5-5" + versionCode 1009000512 + versionName "9.0.5-12" // 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/assets/images/product-illustrations/folder-with-papers.svg b/assets/images/product-illustrations/folder-with-papers.svg new file mode 100644 index 000000000000..3d00fb147ccd --- /dev/null +++ b/assets/images/product-illustrations/folder-with-papers.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__virtualcard.svg b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg new file mode 100644 index 000000000000..2c1f538102a2 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 4232c565e715..cc45e83efa59 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -14,7 +14,7 @@ - + diff --git a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md index ebad41aa2267..38686462a1c2 100644 --- a/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md +++ b/docs/articles/expensify-classic/expensify-card/Cardholder-Settings-and-Features.md @@ -5,7 +5,6 @@ description: Expensify Card Settings for Employees # Using Your Expensify Visa® Commercial Card -## Getting Started ### Activate Your Card You can start using your card immediately upon receipt by logging into your Expensify account, heading to your Home tab, and following the prompts on the _**Activate your Expensify Card**_ task. diff --git a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md index 19972b79d5e0..cb86c340dc81 100644 --- a/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md +++ b/docs/articles/expensify-classic/expensify-card/Dispute-A-Transaction.md @@ -1,50 +1,50 @@ --- title: Expensify Card - Transaction Disputes & Fraud -description: Learn how to dispute an Expensify Card transaction. +description: Understand how to dispute an Expensify Card transaction. --- -# Overview -When using your Expensify Visa® Commercial Card, you may come across transaction errors, which can include things like: -- Unrecognized, unauthorized, or fraudulent charges. -- Transactions of an incorrect amount. +# Disputing Expensify Card Transactions +While using your Expensify Visa® Commercial Card, you might encounter transaction errors, such as: +- Unauthorized transaction activity +- Incorrect transaction amounts. - Duplicate charges for a single transaction. -- Missing a promised merchant refund. +- Missing merchant refunds. -You’ll find all the relevant information on handling these below. +When that happens, you may need to file a dispute for one or more transactions. -# How to Navigate the Dispute Process ## Disputing a Transaction - -If you spot an Expensify Card transaction error, please contact us immediately at [concierge@expensify.com](mailto:concierge@expensify.com). After that, we'll ask a few questions to better understand the situation. If the transaction has already settled in your account (no longer pending), we can file a dispute with our card processor on your behalf. - -If you suspect fraud on your Expensify Card, don't hesitate to cancel it by heading to Settings > Account > Credit Card Import > Request A New Card. Better safe than sorry! - -Lastly, if you haven’t enabled Two-Factor Authentication (2FA) yet, please do so ASAP to add an additional layer of security to your account. +If you notice a transaction error on your Expensify Card, contact us immediately at concierge@expensify.com. We will ask a few questions to understand the situation better, and file a dispute with our card processor on your behalf. ## Types of Disputes +The most common types of disputes are: +- Unauthorized or fraudulent disputes +- Service disputes -There are two main dispute types: +### Unauthorized or fraudulent disputes +- Charges made after your card was lost or stolen. +- Unauthorized charges while your card is in your possession (indicating compromised information). +- Continued charges for a canceled recurring subscription. -1. Unauthorized charges/fraud disputes, which include: - - Charges made with your card after it was lost or stolen. - - Unauthorized charges while your card is still in your possession (indicating compromised card information). - - Continued charges for a canceled recurring subscription. +**If there are transactions made with your Expensify Card you don't recognize, you'll want to do the following right away:** +1. Cancel your card by going to _**Settings > Account > Credit Card Import > Request A New Card**_. +2. Enable Two-Factor Authentication (2FA) for added security under _**Settings > Account > Account Details > Two Factor Authentication**_. -2. Service disputes, which include: - - Received damaged or defective merchandise. - - Charged for merchandise but never received it. - - Double-charged for a purchase made with another method (e.g., cash). - - Made a return but didn't receive a timely refund. - - Multiple charges for a single transaction. - - Charges settled for an incorrect amount. +### Service Disputes +- Received damaged or defective merchandise. +- Charged for merchandise that was never received. +- Double-charged for a purchase made with another method (e.g., cash). +- Made a return but didn't receive a refund. +- Multiple charges for a single transaction. +- Charges settled for an incorrect amount. -You don't need to categorize your dispute; we'll handle that. However, this may help you assess if a situation warrants a dispute. In most cases, the initial step for resolving a dispute should be contacting the merchant, as they can often address the issue promptly. +For service disputes, contacting the merchant is often the quickest way to resolve the dispute. ## Simplifying the Dispute Process - -To ensure the dispute process goes smoothly, please: -- Provide detailed information about the disputed charge, including why you're disputing it, what occurred, and any steps you've taken to address the issue. -- If you recognize the merchant but not the charge, and you've transacted with them before, contact the merchant directly, as it may be a non-fraudulent error. -- Include supporting documentation like receipts or cancellation confirmations when submitting your dispute to enhance the likelihood of a favorable resolution (not required but highly recommended). +To ensure a smooth dispute process, please: +- Provide detailed information about the disputed charge, including why you're disputing it and any steps you've taken to address the issue. +- If you recognize the merchant but not the charge, contact the merchant directly. +- Include supporting documentation (e.g., receipts, cancellation confirmations) when submitting your dispute to increase the chances of a favorable resolution (recommended but not required). +- Make sure the transaction isn't pending (pending transactions cannot be disputed). + {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md index 724745f458ef..b65c66c986ad 100644 --- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md +++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md @@ -2,45 +2,35 @@ title: Request the Card description: Details on requesting the Expensify Card as an employee --- -# Overview - -Once your organization is approved for the Expensify Visa® Commercial Card, you can request a card! - -This article covers how to request, activate, and replace your physical and virtual Expensify Cards. - -# How to get your first Expensify Card - -An admin in your organization must first enable the Expensify Cards before you can receive a card. After that, an admin may assign you a card by setting a limit. You can think of setting a card limit as “unlocking” access to the card. - -If you haven’t been assigned a limit yet, look for the task on your account's homepage that says, “Ask your admin for the card!” This task allows you to message your admin team to make that request. - -Once you’re assigned a card limit, we’ll notify you via email to let you know you can request a card. A link within the notification email will take you to your account’s homepage, where you can provide your shipping address for the physical card. Enter your address, and we’ll ship the card to arrive within 3-5 business days. - -Once your physical card arrives in the mail, activate it in Expensify by entering the last four digits of the card in the activation task on your account’s homepage. - -# Virtual Card - -Once assigned a limit, a virtual card is available immediately. You can view the virtual card details via **Settings > Account > Credit Card Import > Show Details**. Feel free to begin transacting with the virtual card while your physical card is in transit – your virtual card and physical card share a limit. - -Please note that you must enable two-factor authentication on your account if you want to have the option to dispute transactions made on your virtual card. - -# Notifications - -To stay up-to-date on your card’s limit and spending activity, download the Expensify mobile app and enable push notifications. Your card is connected to your Expensify account, so each transaction on your card will trigger a push notification. We’ll also send you a push notification if we detect potentially fraudulent activity and allow you to confirm your purchase. - -# How to request a replacement Expensify Card - -You can request a new card anytime if your Expensify Card is lost, stolen, or damaged. From your Expensify account on the web, head to **Settings > Account > Credit Card Import** and click **Request a New Card**. Confirm the shipping information, complete the prompts, and your new card will arrive in 2 - 3 business days. - -Selecting the “lost” or “stolen” options will deactivate your current card to prevent potentially fraudulent activity. However, choosing the “damaged” option will leave your current card active so you can use it while the new one is shipped to you. - -If you need to cancel your Expensify Card and cannot access the website or mobile app, call our interactive voice recognition phone service (available 24/7). Call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally). - -It's not possible to order a replacement card over the phone, so, if applicable, you would need to handle this step from your Expensify account. - -# Card Expiration Date - -If you notice that your card expiration date is soon, it's time for a new Expensify card. Expensify will automatically input a notification in your account's Home (Inbox) tab. This notice will ask you to input your address, but this is more if you have changed your address since your card was issued to you. You can ignore it and do nothing; the new Expensify card will ship to your address on file. The new Expensify card will have a new, unique card number and will not be associated with the old one. +To start using the Expensify Card, do the following: +1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card. +2. **Request the Card:** + - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” Use this task to message your admin team. + - Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage. + - Enter your address, and the physical card will be shipped within 3-5 business days. +3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage. + +### Virtual Cards +Once you've been assigned a limit, a virtual card is available immediately. You can view its details via _**Settings > Account > Credit Card Import > Show Details**_. + +To protect your account and card spend, enable two-factor authentication under _**Settings > Account > Account Details**_. + +### Notifications +- Download the Expensify mobile app and enable push notifications to stay updated on your card’s limit and spending. +- Each transaction triggers a push notification. +- You’ll also get notifications for potentially fraudulent activity, allowing you to confirm or dispute charges. + +## Request a Replacement Expensify Card +### If the card is lost, stolen, or damaged Card: + - Go to _**Settings > Account > Credit Card Import** and click **Request a New Card**_. + - Confirm your shipping information and complete the prompts. The new card will arrive in 2-3 business days. + - Selecting “lost” or “stolen” deactivates your current card to prevent fraud. Choosing “damaged” keeps the current card active until the new one arrives. + - If you can’t access the website or app, call 1-877-751-5848 (US) or +44 808 196 0632 (Internationally) to cancel your card. + +### If the card is expiring +- If your card is about to expire, Expensify will notify you via your account’s Home (Inbox) tab. +- Enter your address if it has changed; otherwise, do nothing, and the new card will ship to your address on file. +- The new card will have a unique number and will not be linked to the old one. {% include faq-begin.md %} diff --git a/docs/articles/expensify-classic/expensify-card/Statements.md b/docs/articles/expensify-classic/expensify-card/Statements.md index 894dfa3d8b9a..eb797f0cee4b 100644 --- a/docs/articles/expensify-classic/expensify-card/Statements.md +++ b/docs/articles/expensify-classic/expensify-card/Statements.md @@ -1,73 +1,62 @@ --- title: — Expensify Card Statements and Settlements -description: Learn how the Expensify Card statement and settlements work! +description: Understand how to access your Expensify Card Statement --- -# Overview -Expensify offers several settlement types and a statement that provides a detailed view of transactions and settlements. We discuss specifics on both below. +## Expensify Card Statements +Expensify offers several settlement types and a detailed statement of transactions and settlements. -# How to use Expensify Visa® Commercial Card Statement and Settlements -## Using the statement -If your domain uses the Expensify Card and you have a validated Business Bank Account, access the Expensify Card statement at Settings > Domains > Company Cards > Reconciliation Tab > Settlements. +### Accessing the Statement +- If your domain uses the Expensify Card and you have a validated Business Bank Account, access the statement at _**Settings > Domains > Company Cards > Reconciliation Tab > Settlements**_. +- The statement shows individual transactions (debits) and their corresponding settlements (credits). -The Expensify Card statement displays individual transactions (debits) and their corresponding settlements (credits). Each Expensify Cardholder has a Digital Card and a Physical Card, which are treated the same in settlement, reconciliation, and exporting to your accounting system. - -Here's a breakdown of crucial information in the statement: -- **Date:** For card payments, it shows the debit date; for card transactions, it displays the purchase date. -- **Entry ID:** This unique ID groups card payments and transactions together. -- **Withdrawn Amount:** This applies to card payments, matching the debited amount from the Business Bank Account. -- **Transaction Amount:** This applies to card transactions, matching the expense purchase amount. -- **User email:** Applies to card transactions, indicating the cardholder's Expensify email address. -- **Transaction ID:** A unique ID for locating transactions and assisting Expensify Support in case of issues. Transaction IDs are handy for reconciling pre-authorizations. To find the original purchase, locate the Transaction ID in the Settlements tab of the reconciliation dashboard, download the settlements as a CSV, and search for the Transaction ID within it. +### Key Information in the Statement +- **Date:** Debit date for card payments; purchase date for transactions. +- **Entry ID:** Unique ID grouping card payments and transactions. +- **Withdrawn Amount:** Amount debited from the Business Bank Account for card payments. +- **Transaction Amount:** Expense purchase amount for card transactions. +- **User Email:** Cardholder’s Expensify email address. +- **Transaction ID:** Unique ID for locating transactions and assisting support. ![Expanded card settlement that shows the various items that make up each card settlement.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExpanded.png){:width="100%"} -The Expensify Card statement only shows payments from existing Business Bank Accounts under Settings > Account > Payments > Business Accounts. If a Business Account is deleted, the statement won't contain data for payments from that account. - -## Exporting your statement -When using the Expensify Card, you can export your statement to a CSV with these steps: +**Note:** The statement only includes payments from existing Business Bank Accounts under **Settings > Account > Payments > Business Accounts**. Deleted accounts' payments won't appear. - 1. Login to your account on the web app and click on Settings > Domains > Company Cards. - 2. Click the Reconciliation tab at the top right, then select Settlements. - 3. Enter your desired statement dates using the Start and End fields. - 4. Click Search to access the statement for that period. - 5. You can view the table or select Download to export it as a CSV. +## Exporting Statements +1. Log in to the web app and go to **Settings > Domains > Company Cards**. +2. Click the **Reconciliation** tab and select **Settlements**. +3. Enter the start and end dates for your statement. +4. Click **Search** to view the statement. +5. Click **Download** to export it as a CSV. ![Click the Download CSV button in the middle of the page to export your card settlements.](https://help.expensify.com/assets/images/ExpensifyHelp_SettlementExport.png){:width="100%"} ## Expensify Card Settlement Frequency -Paying your Expensify Card balance is simple with automatic settlement. There are two settlement frequency options: - - **Daily Settlement:** Your Expensify Card balance is paid in full every business day, meaning you’ll see an itemized debit each business day. - - **Monthly Settlement:** Expensify Cards are settled monthly, with your settlement date determined during the card activation process. With monthly, you’ll see only one itemized debit per month. (Available for Plaid-connected bank accounts with no recent negative balance.) +- **Daily Settlement:** Balance paid in full every business day with an itemized debit each day. +- **Monthly Settlement:** Balance settled monthly on a predetermined date with one itemized debit per month (available for Plaid-connected accounts with no recent negative balance). -## How settlement works -Each business day (Monday through Friday, excluding US bank holidays) or on your monthly settlement date, we calculate the total of posted Expensify Card transactions since the last settlement. The settlement amount represents what you must pay to bring your Expensify Card balance back to $0. +## How Settlement Works +- Each business day or on your monthly settlement date, the total of posted transactions is calculated. +- The settlement amount is withdrawn from the Verified Business Bank Account linked to the primary domain admin, resetting your card balance to $0. +- To change your settlement frequency or bank account, go to _**Settings > Domains > [Domain Name] > Company Cards**_, click the **Settings** tab, and select the new options from the dropdown menu. Click **Save** to confirm. -We'll automatically withdraw this settlement amount from the Verified Business Bank Account linked to the primary domain admin. You can set up this bank account in the web app under Settings > Account > Payments > Bank Accounts. +![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"} -Once the payment is made, your Expensify Card balance will be $0, and the transactions are considered "settled." -To change your settlement frequency or bank account, go to Settings > Domains > [Domain Name] > Company Cards. On the Company Cards page, click the Settings tab, choose a new settlement frequency or account from the dropdown menu, and click Save to confirm the change. +# FAQ -![Change your card settlement account or settlement frequency via the dropdown menus in the middle of the screen.](https://help.expensify.com/assets/images/ExpensifyHelp_CardSettings.png){:width="100%"} +## Can you pay your balance early if you’ve reached your Domain Limit? +- For Monthly Settlement, use the “Settle Now” button to manually initiate settlement. +- For Daily Settlement, balances settle automatically with no additional action required. -# Expensify Card Statement and Settlements FAQs -## Can you pay your balance early if you've reached your Domain Limit? -If you've chosen Monthly Settlement, you can manually initiate settlement using the "Settle Now" button. We'll settle the outstanding balance and then perform settlement again on your selected predetermined monthly settlement date. - -If you opt for Daily Settlement, the Expensify Card statement will automatically settle daily through an automatic withdrawal from your business bank account. No additional action is needed on your part. - ## Will our domain limit change if our Verified Bank Account has a higher balance? -Your domain limit may fluctuate based on your cash balance, spending patterns, and history with Expensify. Suppose you've recently transferred funds to the business bank account linked to Expensify card settlements. In that case, you should expect a change in your domain limit within 24 hours of the transfer (assuming your business bank account is connected through Plaid). - +Domain limits may change based on cash balance, spending patterns, and history with Expensify. If your bank account is connected through Plaid, expect changes within 24 hours of transferring funds. + ## How is the “Amount Owed” figure on the card list calculated? -The amount owed consists of all Expensify Card transactions, both pending and posted, since the last settlement date. The settlement amount withdrawn from your designated Verified Business Bank Account only includes posted transactions. - -Your amount owed decreases when the settlement clears. Any pending transactions that don't post timely will automatically expire, reducing your amount owed. - -## **How do I view all unsettled expenses?** -To view unsettled expenses since the last settlement, use the Reconciliation Dashboard's Expenses tab. Follow these steps: - 1. Note the dates of expenses in your last settlement. - 2. Switch to the Expenses tab on the Reconciliation Dashboard. - 3. Set the start date just after the last settled expenses and the end date to today. - 4. The Imported Total will show the outstanding amount, and you can click through to view individual expenses. +It includes all pending and posted transactions since the last settlement date. The settlement amount withdrawn only includes posted transactions. + +## How do I view all unsettled expenses? +1. Note the dates of expenses in your last settlement. +2. Go to the **Expenses** tab on the Reconciliation Dashboard. +3. Set the start date after the last settled expenses and the end date to today. +4. The **Imported Total** shows the outstanding amount, and you can click to view individual expenses. diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 881a91fcee5d..2ccce98e6a21 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.5.5 + 9.0.5.12 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index dd3dbc8264da..8248e7db0454 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.5.5 + 9.0.5.12 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index c75fa81b19e1..87cdb420af38 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.5 CFBundleVersion - 9.0.5.5 + 9.0.5.12 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a5ffdcb4b63c..29ab90c4b7db 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -282,7 +282,7 @@ PODS: - nanopb/encode (= 2.30908.0) - nanopb/decode (2.30908.0) - nanopb/encode (2.30908.0) - - Onfido (29.7.1) + - Onfido (29.7.2) - onfido-react-native-sdk (10.6.0): - glog - hermes-engine @@ -1871,7 +1871,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.88): + - RNLiveMarkdown (0.1.91): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1889,9 +1889,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.88) + - RNLiveMarkdown/common (= 0.1.91) - Yoga - - RNLiveMarkdown/common (0.1.88): + - RNLiveMarkdown/common (0.1.91): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2531,7 +2531,7 @@ SPEC CHECKSUMS: MapboxMaps: 87ef0003e6db46e45e7a16939f29ae87e38e7ce2 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 + Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2 Plaid: 7829e84db6d766a751c91a402702946d2977ddcb PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 @@ -2614,7 +2614,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: e33d2c97863d5480f8f4b45f8b25f801cc43c7f5 + RNLiveMarkdown: 24fbb7370eefee2f325fb64cfe904b111ffcd81b RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: df8fe93dbd251f25022f4023d31bc04160d4d65c RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 @@ -2631,7 +2631,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: d5e281e5370cb0211a104efd90eb5fa7af936e14 diff --git a/package-lock.json b/package-lock.json index 2871cf3c1bb0..c401dfe77198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "new.expensify", - "version": "9.0.5-5", + "version": "9.0.5-12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.5-5", + "version": "9.0.5-12", "hasInstallScript": true, "license": "MIT", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.88", + "@expensify/react-native-live-markdown": "0.1.91", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -55,7 +55,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.19", + "expensify-common": "2.0.26", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -79,7 +79,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.13", + "react-fast-pdf": "1.0.14", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -3784,9 +3784,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.88", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.88.tgz", - "integrity": "sha512-78X5ACV+OL+aL6pfJAXyHkNuMGUc4Rheo4qLkIwLpmUIAiAxmY0B2lch5XHSNGf1a5ofvVbdQ6kl84+4E6DwlQ==", + "version": "0.1.91", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.91.tgz", + "integrity": "sha512-6uQTgwhpvLqQKdtNqSgh45sRuQRXzv/WwyhdvQNge6EYtulyGFqT82GIP+LIGW8Xnl73nzFZTuMKwWxFFR/Cow==", "workspaces": [ "parser", "example", @@ -25974,9 +25974,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.19.tgz", - "integrity": "sha512-GdWlYiHOAapy/jxjcvL9NKGOofhoEuKIwvJNGNVHbDXcA+0NxVCNYrHt1yrLnVcE4KtK6PGT6fQ2Lp8NTCoA+g==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.26.tgz", + "integrity": "sha512-3GORs2xfx78SoKLDh4lXpk4Bx61sAVNnlo23VB803zs7qZz8/Oq3neKedtEJuRAmUps0C1Y5y9xZE8nrPO31nQ==", "dependencies": { "awesome-phonenumber": "^5.4.0", "classnames": "2.5.0", @@ -25988,7 +25988,7 @@ "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.6.0", + "semver": "^7.6.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "ua-parser-js": "^1.0.37" } @@ -36987,9 +36987,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.13.tgz", - "integrity": "sha512-rF7NQZ26rJAI8ysRJaG71dl2c7AIq48ibbn7xCyF3lEZ/yOjA8BeR0utRwDjaHGtswQscgETboilhaaH5UtIYg==", + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.14.tgz", + "integrity": "sha512-iWomykxvnZtokIKpRK5xpaRfXz9ufrY7AVANtIBYsAZtX5/7VDlpIQwieljfMZwFc96TyceCnneufsgXpykTQw==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" @@ -39473,11 +39473,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, diff --git a/package.json b/package.json index 3ff6021ced4f..5420a3e886ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.5-5", + "version": "9.0.5-12", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -67,7 +67,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.88", + "@expensify/react-native-live-markdown": "0.1.91", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -109,7 +109,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.19", + "expensify-common": "2.0.26", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -133,7 +133,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "1.0.13", + "react-fast-pdf": "1.0.14", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", diff --git a/patches/@expensify+react-native-live-markdown+0.1.85.patch b/patches/@expensify+react-native-live-markdown+0.1.85.patch deleted file mode 100644 index f745786a088e..000000000000 --- a/patches/@expensify+react-native-live-markdown+0.1.85.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -index e975fb2..6a4b510 100644 ---- a/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -+++ b/node_modules/@expensify/react-native-live-markdown/lib/module/web/cursorUtils.js -@@ -53,7 +53,7 @@ function setCursorPosition(target, start, end = null) { - // 3. Caret at the end of whole input, when pressing enter - // 4. All other placements - if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) { -- if (nextChar !== '\n') { -+ if (nextChar !== '\n' && i !== n - 1 && nextChar) { - range.setStart(textNodes[i + 1], 0); - } else if (i !== textNodes.length - 1) { - range.setStart(textNodes[i], 1); diff --git a/patches/@expensify+react-native-live-markdown+0.1.91.patch b/patches/@expensify+react-native-live-markdown+0.1.91.patch new file mode 100644 index 000000000000..c77e46accae3 --- /dev/null +++ b/patches/@expensify+react-native-live-markdown+0.1.91.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts +index 1cda659..ba5c3c3 100644 +--- a/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts ++++ b/node_modules/@expensify/react-native-live-markdown/src/web/cursorUtils.ts +@@ -66,7 +66,7 @@ function setCursorPosition(target: HTMLElement, start: number, end: number | nul + // 3. Caret at the end of whole input, when pressing enter + // 4. All other placements + if (prevChar === '\n' && prevTextLength !== undefined && prevTextLength < textCharacters.length) { +- if (nextChar !== '\n') { ++ if (nextChar && nextChar !== '\n' && i !== n - 1) { + range.setStart(textNodes[i + 1] as Node, 0); + } else if (i !== textNodes.length - 1) { + range.setStart(textNodes[i] as Node, 1); diff --git a/patches/react-native-keyboard-controller+1.12.2.patch.patch b/patches/react-native-keyboard-controller+1.12.2.patch similarity index 100% rename from patches/react-native-keyboard-controller+1.12.2.patch.patch rename to patches/react-native-keyboard-controller+1.12.2.patch diff --git a/patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch b/patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2+001+fix-boost-dependency.patch rename to patches/react-native-reanimated+3.8.1+001+fix-boost-dependency.patch diff --git a/patches/react-native-reanimated+3.7.2+002+copy-state.patch b/patches/react-native-reanimated+3.8.1+002+copy-state.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2+002+copy-state.patch rename to patches/react-native-reanimated+3.8.1+002+copy-state.patch diff --git a/patches/react-native-reanimated+3.7.2.patch b/patches/react-native-reanimated+3.8.1.patch similarity index 100% rename from patches/react-native-reanimated+3.7.2.patch rename to patches/react-native-reanimated+3.8.1.patch diff --git a/scripts/applyPatches.sh b/scripts/applyPatches.sh new file mode 100755 index 000000000000..a4be88984561 --- /dev/null +++ b/scripts/applyPatches.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# This script is a simple wrapper around patch-package that fails if any errors or warnings are detected. +# This is useful because patch-package does not fail on errors or warnings by default, +# which means that broken patches are easy to miss, and leads to developer frustration and wasted time. + +SCRIPTS_DIR=$(dirname "${BASH_SOURCE[0]}") +source "$SCRIPTS_DIR/shellUtils.sh" + +# Wrapper to run patch-package. +# We use `script` to preserve colorization when the output of patch-package is piped to tee +# and we provide /dev/null to discard the output rather than sending it to a file +# `script` has different syntax on macOS vs linux, so that's why we need a wrapper function +function patchPackage { + OS="$(uname)" + if [[ "$OS" == "Darwin" ]]; then + # macOS + script -q /dev/null npx patch-package --error-on-fail + elif [[ "$OS" == "Linux" ]]; then + # Ubuntu/Linux + script -q -c "npx patch-package --error-on-fail" /dev/null + else + error "Unsupported OS: $OS" + fi +} + +# Run patch-package and capture its output and exit code, while still displaying the original output to the terminal +# (we use `script -q /dev/null` to preserve colorization in the output) +TEMP_OUTPUT="$(mktemp)" +patchPackage 2>&1 | tee "$TEMP_OUTPUT" +EXIT_CODE=${PIPESTATUS[0]} +OUTPUT="$(cat "$TEMP_OUTPUT")" +rm -f "$TEMP_OUTPUT" + +# Check if the output contains a warning message +echo "$OUTPUT" | grep -q "Warning:" +WARNING_FOUND=$? + +printf "\n"; + +# Determine the final exit code +if [ "$EXIT_CODE" -eq 0 ]; then + if [ $WARNING_FOUND -eq 0 ]; then + # patch-package succeeded but warning was found + error "It looks like you upgraded a dependency without upgrading the patch. Please review the patch, determine if it's still needed, and port it to the new version of the dependency." + exit 1 + else + # patch-package succeeded and no warning was found + success "patch-package succeeded without errors or warnings" + exit 0 + fi +else + # patch-package failed + error "patch-package failed to apply a patch" + exit "$EXIT_CODE" +fi diff --git a/scripts/postInstall.sh b/scripts/postInstall.sh index 339fdf25cb10..782c8ef5822c 100755 --- a/scripts/postInstall.sh +++ b/scripts/postInstall.sh @@ -1,11 +1,14 @@ #!/bin/bash +# Exit immediately if any command exits with a non-zero status +set -e + # Go to project root ROOT_DIR=$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)") cd "$ROOT_DIR" || exit 1 -# Run patch-package -npx patch-package +# Apply packages using patch-package +scripts/applyPatches.sh # Install node_modules in subpackages, unless we're in a CI/CD environment, # where the node_modules for subpackages are cached separately. diff --git a/src/App.tsx b/src/App.tsx index 21025d34a661..98b5d4afeb1d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; import SafeArea from './components/SafeArea'; import ScrollOffsetContextProvider from './components/ScrollOffsetContextProvider'; +import {SearchContextProvider} from './components/Search/SearchContext'; import ThemeIllustrationsProvider from './components/ThemeIllustrationsProvider'; import ThemeProvider from './components/ThemeProvider'; import ThemeStylesProvider from './components/ThemeStylesProvider'; @@ -91,6 +92,7 @@ function App({url}: AppProps) { VolumeContextProvider, VideoPopoverMenuContextProvider, KeyboardProvider, + SearchContextProvider, ]} > diff --git a/src/CONST.ts b/src/CONST.ts index 13d44ee883be..42be5a24cca3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -609,6 +609,7 @@ const CONST = { TRAVEL_TERMS_URL: `${USE_EXPENSIFY_URL}/travelterms`, EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT: 'https://www.expensify.com/tools/integrations/downloadPackage', EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct', + SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct', HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct', PRICING: `https://www.expensify.com/pricing`, @@ -1345,18 +1346,75 @@ const CONST = { }, }, + SAGE_INTACCT_MAPPING_VALUE: { + NONE: 'NONE', + DEFAULT: 'DEFAULT', + TAG: 'TAG', + REPORT_FIELD: 'REPORT_FIELD', + }, + + SAGE_INTACCT_CONFIG: { + MAPPINGS: { + DEPARTMENTS: 'departments', + CLASSES: 'classes', + LOCATIONS: 'locations', + CUSTOMERS: 'customers', + PROJECTS: 'projects', + }, + SYNC_ITEMS: 'syncItems', + TAX: 'tax', + EXPORT: 'export', + EXPORT_DATE: 'exportDate', + NON_REIMBURSABLE_CREDIT_CARD_VENDOR: 'nonReimbursableCreditCardChargeDefaultVendor', + NON_REIMBURSABLE_VENDOR: 'nonReimbursableVendor', + REIMBURSABLE_VENDOR: 'reimbursableExpenseReportDefaultVendor', + NON_REIMBURSABLE_ACCOUNT: 'nonReimbursableAccount', + NON_REIMBURSABLE: 'nonReimbursable', + EXPORTER: 'exporter', + REIMBURSABLE: 'reimbursable', + AUTO_SYNC: 'autoSync', + AUTO_SYNC_ENABLED: 'enabled', + IMPORT_EMPLOYEES: 'importEmployees', + APPROVAL_MODE: 'approvalMode', + SYNC: 'sync', + SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports', + REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID', + }, + + SAGE_INTACCT: { + APPROVAL_MODE: { + APPROVAL_MANUAL: 'APPROVAL_MANUAL', + }, + }, + QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE: { VENDOR_BILL: 'bill', CHECK: 'check', JOURNAL_ENTRY: 'journal_entry', }, + SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE: { + EXPENSE_REPORT: 'EXPENSE_REPORT', + VENDOR_BILL: 'VENDOR_BILL', + }, + + SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE: { + CREDIT_CARD_CHARGE: 'CREDIT_CARD_CHARGE', + VENDOR_BILL: 'VENDOR_BILL', + }, + XERO_EXPORT_DATE: { LAST_EXPENSE: 'LAST_EXPENSE', REPORT_EXPORTED: 'REPORT_EXPORTED', REPORT_SUBMITTED: 'REPORT_SUBMITTED', }, + SAGE_INTACCT_EXPORT_DATE: { + LAST_EXPENSE: 'LAST_EXPENSE', + EXPORTED: 'EXPORTED', + SUBMITTED: 'SUBMITTED', + }, + NETSUITE_CONFIG: { SUBSIDIARY: 'subsidiary', EXPORTER: 'exporter', @@ -1389,7 +1447,12 @@ const CONST = { 3: 'createAccessToken', 4: 'enterCredentials', }, - IMPORT_CUSTOM_FIELDS: ['customSegments', 'customLists'], + IMPORT_CUSTOM_FIELDS: { + CUSTOM_SEGMENTS: 'customSegments', + CUSTOM_LISTS: 'customLists', + }, + CUSTOM_SEGMENT_FIELDS: ['segmentName', 'internalID', 'scriptID', 'mapping'], + CUSTOM_LIST_FIELDS: ['listName', 'internalID', 'transactionFieldID', 'mapping'], CUSTOM_FORM_ID_TYPE: { REIMBURSABLE: 'reimbursable', NON_REIMBURSABLE: 'nonReimbursable', @@ -1408,6 +1471,40 @@ const CONST = { JOBS: 'jobs', }, }, + NETSUITE_CUSTOM_LIST_LIMIT: 8, + NETSUITE_ADD_CUSTOM_LIST_STEP_NAMES: ['1', '2,', '3', '4'], + NETSUITE_ADD_CUSTOM_SEGMENT_STEP_NAMES: ['1', '2,', '3', '4', '5', '6,'], + }, + + NETSUITE_CUSTOM_FIELD_SUBSTEP_INDEXES: { + CUSTOM_LISTS: { + CUSTOM_LIST_PICKER: 0, + TRANSACTION_FIELD_ID: 1, + MAPPING: 2, + CONFIRM: 3, + }, + CUSTOM_SEGMENTS: { + SEGMENT_TYPE: 0, + SEGMENT_NAME: 1, + INTERNAL_ID: 2, + SCRIPT_ID: 3, + MAPPING: 4, + CONFIRM: 5, + }, + }, + + NETSUITE_CUSTOM_RECORD_TYPES: { + CUSTOM_SEGMENT: 'customSegment', + CUSTOM_RECORD: 'customRecord', + }, + + NETSUITE_FORM_STEPS_HEADER_HEIGHT: 40, + + NETSUITE_IMPORT: { + HELP_LINKS: { + CUSTOM_SEGMENTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-segments', + CUSTOM_LISTS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/NetSuite#custom-lists', + }, }, NETSUITE_EXPORT_DATE: { @@ -2130,6 +2227,10 @@ const CONST = { CARD_NAME: 'CardName', CONFIRMATION: 'Confirmation', }, + CARD_TYPE: { + PHYSICAL: 'physical', + VIRTUAL: 'virtual', + }, }, AVATAR_ROW_SIZE: { DEFAULT: 4, @@ -3796,6 +3897,7 @@ const CONST = { }, EVENTS: { SCROLLING: 'scrolling', + ON_RETURN_TO_OLD_DOT: 'onReturnToOldDot', }, CHAT_HEADER_LOADER_HEIGHT: 36, @@ -5062,6 +5164,9 @@ const CONST = { DONE: 'done', PAID: 'paid', VIEW: 'view', + REVIEW: 'review', + HOLD: 'hold', + UNHOLD: 'unhold', }, TRANSACTION_TYPE: { CASH: 'cash', @@ -5159,6 +5264,10 @@ const CONST = { DATE: 'date', LIST: 'dropdown', }, + + NAVIGATION_ACTIONS: { + RESET: 'RESET', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5d6b5492d15c..bd4b294a6d68 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -320,6 +320,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding error message to be displayed to the user */ + ONBOARDING_ERROR_MESSAGE: 'onboardingErrorMessage', + /** Onboarding policyID selected by the user during Onboarding flow */ ONBOARDING_POLICY_ID: 'onboardingPolicyID', @@ -460,8 +463,8 @@ const ONYXKEYS = { WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', WORKSPACE_TAX_CUSTOM_NAME: 'workspaceTaxCustomName', WORKSPACE_TAX_CUSTOM_NAME_DRAFT: 'workspaceTaxCustomNameDraft', - WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldsForm', - WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldsFormDraft', + WORKSPACE_REPORT_FIELDS_FORM: 'workspaceReportFieldForm', + WORKSPACE_REPORT_FIELDS_FORM_DRAFT: 'workspaceReportFieldFormDraft', POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm', @@ -556,14 +559,22 @@ const ONYXKEYS = { NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', - ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCardForm', - ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardFormDraft', + ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', + ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', + NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm', + NETSUITE_CUSTOM_FIELD_FORM_DRAFT: 'netSuiteCustomFieldFormDraft', + NETSUITE_CUSTOM_SEGMENT_ADD_FORM: 'netSuiteCustomSegmentAddForm', + NETSUITE_CUSTOM_SEGMENT_ADD_FORM_DRAFT: 'netSuiteCustomSegmentAddFormDraft', + NETSUITE_CUSTOM_LIST_ADD_FORM: 'netSuiteCustomListAddForm', + NETSUITE_CUSTOM_LIST_ADD_FORM_DRAFT: 'netSuiteCustomListAddFormDraft', NETSUITE_TOKEN_INPUT_FORM: 'netsuiteTokenInputForm', NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft', NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm', NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft', + SAGE_INTACCT_DIMENSION_TYPE_FORM: 'sageIntacctDimensionTypeForm', + SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft', }, } as const; @@ -576,7 +587,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; - [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldsForm; + [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; @@ -627,8 +638,12 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; + [ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; [ONYXKEYS.FORMS.NETSUITE_TOKEN_INPUT_FORM]: FormTypes.NetSuiteTokenInputForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm; + [ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm; }; type OnyxFormDraftValuesMapping = { @@ -782,6 +797,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string; [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d548297cb854..a54bb4f5cca5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3,6 +3,7 @@ import type CONST from './CONST'; import type {IOUAction, IOUType} from './CONST'; import type {IOURequestType} from './libs/actions/IOU'; import type {AuthScreensParamList} from './libs/Navigation/types'; +import type {SageIntacctMappingName} from './types/onyx/Policy'; import type {SearchQuery} from './types/onyx/SearchResults'; import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; @@ -53,6 +54,11 @@ const ROUTES = { getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, }, + TRANSACTION_HOLD_REASON_RHP: { + route: '/search/:query/hold/:transactionID', + getRoute: (query: string, transactionID: string) => `search/${query}/hold/${transactionID}` as const, + }, + // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', FLAG_COMMENT: { @@ -787,41 +793,43 @@ const ROUTES = { route: 'settings/workspaces/:policyID/reportFields', getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const, }, - WORKSPACE_REPORT_FIELD_SETTINGS: { - route: 'settings/workspaces/:policyID/reportField/:reportFieldKey', - getRoute: (policyID: string, reportFieldKey: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldKey)}` as const, - }, WORKSPACE_CREATE_REPORT_FIELD: { route: 'settings/workspaces/:policyID/reportFields/new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new` as const, }, + WORKSPACE_REPORT_FIELD_SETTINGS: { + route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit` as const, + }, WORKSPACE_REPORT_FIELD_LIST_VALUES: { - route: 'settings/workspaces/:policyID/reportFields/new/listValues', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/listValues` as const, + route: 'settings/workspaces/:policyID/reportField/listValues/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, }, WORKSPACE_REPORT_FIELD_ADD_VALUE: { - route: 'settings/workspaces/:policyID/reportFields/new/addValue', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields/new/addValue` as const, + route: 'settings/workspaces/:policyID/reportField/addValue/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportField/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, }, WORKSPACE_REPORT_FIELD_VALUE_SETTINGS: { - route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex', - getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}` as const, + route: 'settings/workspaces/:policyID/reportField/:valueIndex/:reportFieldID?', + getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => + `settings/workspaces/${policyID}/reportField/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, }, WORKSPACE_REPORT_FIELD_EDIT_VALUE: { - route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', - getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportFields/new/${valueIndex}/edit` as const, + route: 'settings/workspaces/:policyID/reportField/new/:valueIndex/edit', + getRoute: (policyID: string, valueIndex: number) => `settings/workspaces/${policyID}/reportField/new/${valueIndex}/edit` as const, + }, + WORKSPACE_EDIT_REPORT_FIELD_INITIAL_VALUE: { + route: 'settings/workspaces/:policyID/reportField/:reportFieldID/edit/initialValue', + getRoute: (policyID: string, reportFieldID: string) => `settings/workspaces/${policyID}/reportField/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, }, WORKSPACE_EXPENSIFY_CARD: { route: 'settings/workspaces/:policyID/expensify-card', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, }, - // TODO: uncomment after development is done - // WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { - // route: 'settings/workspaces/:policyID/expensify-card/issues-new', - // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, - // }, - // TODO: remove after development is done - this one is for testing purposes - WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: 'settings/workspaces/expensify-card/issue-new', + WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { + route: 'settings/workspaces/:policyID/expensify-card/issue-new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, @@ -1009,6 +1017,29 @@ const ROUTES = { getRoute: (policyID: string, importField: TupleToUnion) => `settings/workspaces/${policyID}/accounting/netsuite/import/mapping/${importField}` as const, }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_MAPPING: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField', + getRoute: (policyID: string, importCustomField: ValueOf) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/view/:valueIndex', + getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/view/${valueIndex}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom/:importCustomField/edit/:valueIndex/:fieldName', + getRoute: (policyID: string, importCustomField: ValueOf, valueIndex: number, fieldName: string) => + `settings/workspaces/${policyID}/accounting/netsuite/import/custom/${importCustomField}/edit/${valueIndex}/${fieldName}` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_LIST_ADD: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-list/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-list/new` as const, + }, + POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: { + route: 'settings/workspaces/:policyID/accounting/netsuite/import/custom-segment/new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/custom-segment/new` as const, + }, POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: { route: 'settings/workspaces/:policyID/accounting/netsuite/import/customer-projects', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/netsuite/import/customer-projects` as const, @@ -1119,6 +1150,66 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/sage-intacct/existing-connections', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/existing-connections` as const, }, + POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_TOGGLE_MAPPINGS: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/toggle-mapping/:mapping', + getRoute: (policyID: string, mapping: SageIntacctMappingName) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/toggle-mapping/${mapping}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/mapping-type/:mapping', + getRoute: (policyID: string, mapping: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/mapping-type/${mapping}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_USER_DIMENSIONS: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/user-dimensions', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/user-dimensions` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_ADD_USER_DIMENSION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/add-user-dimension', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/add-user-dimension` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EDIT_USER_DIMENSION: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/import/edit-user-dimension/:dimensionName', + getRoute: (policyID: string, dimensionName: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/import/edit-user-dimension/${dimensionName}` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/preferred-exporter', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/preferred-exporter` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/date', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/date` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_REIMBURSABLE_EXPENSES: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/reimbursable', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/reimbursable` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/:reimbursable/default-vendor', + getRoute: (policyID: string, reimbursable: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/${reimbursable}/default-vendor` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/export/nonreimbursable/credit-card-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/export/nonreimbursable/credit-card-account` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced` as const, + }, + POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT: { + route: 'settings/workspaces/:policyID/accounting/sage-intacct/advanced/payment-account', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/advanced/payment-account` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e2e3f7acc5b0..d2a6b7c19ddd 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -30,6 +30,7 @@ const SCREENS = { SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', REPORT_RHP: 'Search_Report_RHP', + TRANSACTION_HOLD_REASON_RHP: 'Search_Transaction_Hold_Reason_RHP', BOTTOM_TAB: 'Search_Bottom_Tab', }, SETTINGS: { @@ -280,6 +281,11 @@ const SCREENS = { XERO_BILL_PAYMENT_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Bill_Payment_Account_Selector', XERO_EXPORT_BANK_ACCOUNT_SELECT: 'Policy_Accounting_Xero_Export_Bank_Account_Select', NETSUITE_IMPORT_MAPPING: 'Policy_Accounting_NetSuite_Import_Mapping', + NETSUITE_IMPORT_CUSTOM_FIELD: 'Policy_Accounting_NetSuite_Import_Custom_Field', + NETSUITE_IMPORT_CUSTOM_FIELD_VIEW: 'Policy_Accounting_NetSuite_Import_Custom_Field_View', + NETSUITE_IMPORT_CUSTOM_FIELD_EDIT: 'Policy_Accounting_NetSuite_Import_Custom_Field_Edit', + NETSUITE_IMPORT_CUSTOM_LIST_ADD: 'Policy_Accounting_NetSuite_Import_Custom_List_Add', + NETSUITE_IMPORT_CUSTOM_SEGMENT_ADD: 'Policy_Accounting_NetSuite_Import_Custom_Segment_Add', NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects', NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS_SELECT: 'Policy_Accounting_NetSuite_Import_CustomersOrProjects_Select', NETSUITE_TOKEN_INPUT: 'Policy_Accounting_NetSuite_Token_Input', @@ -309,6 +315,21 @@ const SCREENS = { SAGE_INTACCT_PREREQUISITES: 'Policy_Accounting_Sage_Intacct_Prerequisites', ENTER_SAGE_INTACCT_CREDENTIALS: 'Policy_Enter_Sage_Intacct_Credentials', EXISTING_SAGE_INTACCT_CONNECTIONS: 'Policy_Existing_Sage_Intacct_Connections', + SAGE_INTACCT_IMPORT: 'Policy_Accounting_Sage_Intacct_Import', + SAGE_INTACCT_TOGGLE_MAPPING: 'Policy_Accounting_Sage_Intacct_Toggle_Mapping', + SAGE_INTACCT_MAPPING_TYPE: 'Policy_Accounting_Sage_Intacct_Mapping_Type', + SAGE_INTACCT_USER_DIMENSIONS: 'Policy_Accounting_Sage_Intacct_User_Dimensions', + SAGE_INTACCT_ADD_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Add_User_Dimension', + SAGE_INTACCT_EDIT_USER_DIMENSION: 'Policy_Accounting_Sage_Intacct_Edit_User_Dimension', + SAGE_INTACCT_EXPORT: 'Policy_Accounting_Sage_Intacct_Export', + SAGE_INTACCT_PREFERRED_EXPORTER: 'Policy_Accounting_Sage_Intacct_Preferred_Exporter', + SAGE_INTACCT_EXPORT_DATE: 'Policy_Accounting_Sage_Intacct_Export_Date', + SAGE_INTACCT_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Reimbursable_Expenses', + SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Expenses', + SAGE_INTACCT_DEFAULT_VENDOR: 'Policy_Accounting_Sage_Intacct_Default_Vendor', + SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account', + SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced', + SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account', }, INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Profile', @@ -338,6 +359,7 @@ const SCREENS = { REPORT_FIELDS_ADD_VALUE: 'Workspace_ReportFields_AddValue', REPORT_FIELDS_VALUE_SETTINGS: 'Workspace_ReportFields_ValueSettings', REPORT_FIELDS_EDIT_VALUE: 'Workspace_ReportFields_EditValue', + REPORT_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_ReportFields_EditInitialValue', TAX_EDIT: 'Workspace_Tax_Edit', TAX_NAME: 'Workspace_Tax_Name', TAX_VALUE: 'Workspace_Tax_Value', diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index bd9b623bcfb4..2b2c53eaaa18 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -13,6 +13,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import {removePolicyConnection} from '@libs/actions/connections'; import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline'; +import * as PolicyAction from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Session} from '@src/types/onyx'; @@ -49,6 +50,8 @@ function ConnectToQuickbooksOnlineButton({ setIsDisconnectModalOpen(true); return; } + // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO + PolicyAction.enablePolicyTaxes(policyID, false); setWebViewOpen(true); }} text={translate('workspace.accounting.setup')} @@ -59,6 +62,8 @@ function ConnectToQuickbooksOnlineButton({ {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( { + // Since QBO doesn't support Taxes, we should disable them from the LHN when connecting to QBO + PolicyAction.enablePolicyTaxes(policyID, false); removePolicyConnection(policyID, integrationToDisconnect); setIsDisconnectModalOpen(false); setWebViewOpen(true); diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx index 4bcfdc61077f..3809b4f4f110 100644 --- a/src/components/ConnectionLayout.tsx +++ b/src/components/ConnectionLayout.tsx @@ -61,8 +61,8 @@ type ConnectionLayoutProps = { /** Name of the current connection */ connectionName: ConnectionName; - /** Block the screen when the connection is not empty */ - reverseConnectionEmptyCheck?: boolean; + /** Whether the screen should load for an empty connection */ + shouldLoadForEmptyConnection?: boolean; /** Handler for back button press */ onBackButtonPress?: () => void; @@ -100,7 +100,7 @@ function ConnectionLayout({ shouldUseScrollView = true, headerTitleAlreadyTranslated, titleAlreadyTranslated, - reverseConnectionEmptyCheck = false, + shouldLoadForEmptyConnection = false, onBackButtonPress = () => Navigation.goBack(), shouldBeBlocked = false, }: ConnectionLayoutProps) { @@ -122,12 +122,14 @@ function ConnectionLayout({ [title, titleStyle, children, titleAlreadyTranslated], ); + const shouldBlockByConnection = shouldLoadForEmptyConnection ? !isConnectionEmpty : isConnectionEmpty; + return ( { + if (ReportActionComposeFocusManager.isFocused()) { + return false; + } + return element; + }, }} > {children} diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index a4b9df5916af..793154d95b02 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -73,6 +73,9 @@ type FormProviderProps = FormProvider /** Whether to apply flex to the submit button */ submitFlexEnabled?: boolean; + + /** Whether button is disabled */ + isSubmitDisabled?: boolean; }; function FormProvider( diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 5c74fd466a15..77ef44343792 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -38,6 +38,9 @@ type FormWrapperProps = ChildrenProps & /** Assuming refs are React refs */ inputRefs: RefObject; + /** Whether the submit button is disabled */ + isSubmitDisabled?: boolean; + /** Callback to submit the form */ onSubmit: () => void; }; @@ -57,9 +60,11 @@ function FormWrapper({ enabledWhenOffline, isSubmitActionDangerous = false, formID, + shouldUseScrollView = true, scrollContextEnabled = false, shouldHideFixErrorsAlert = false, disablePressOnEnter = true, + isSubmitDisabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -108,6 +113,7 @@ function FormWrapper({ {isSubmitButtonVisible && ( {({safeAreaPaddingBottomStyle}) => diff --git a/src/components/Form/InputWrapper.tsx b/src/components/Form/InputWrapper.tsx index b5535a2fe6c1..c966dd4456e9 100644 --- a/src/components/Form/InputWrapper.tsx +++ b/src/components/Form/InputWrapper.tsx @@ -54,7 +54,7 @@ function computeComponentSpecificRegistrationParams({ shouldSetTouchedOnBlurOnly: false, // Forward the originally provided value blurOnSubmit, - shouldSubmitForm: false, + shouldSubmitForm: !!shouldSubmitForm, }; } diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index afbe2bb124b5..5f56bbeceea6 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -20,6 +20,10 @@ import type TextInput from '@components/TextInput'; import type TextPicker from '@components/TextPicker'; import type ValuePicker from '@components/ValuePicker'; import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker'; +import type DimensionTypeSelector from '@pages/workspace/accounting/intacct/import/DimensionTypeSelector'; +import type NetSuiteCustomFieldMappingPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomFieldMappingPicker'; +import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteCustomListPicker'; +import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm'; import type {Country} from '@src/CONST'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; import type {BaseForm} from '@src/types/form/Form'; @@ -39,6 +43,7 @@ type ValidInputs = | typeof CurrencySelector | typeof AmountForm | typeof BusinessTypePicker + | typeof DimensionTypeSelector | typeof StateSelector | typeof RoomNameInput | typeof ValuePicker @@ -47,7 +52,10 @@ type ValidInputs = | typeof AmountPicker | typeof TextPicker | typeof AddPlaidBankAccount - | typeof EmojiPickerButtonDropdown; + | typeof EmojiPickerButtonDropdown + | typeof NetSuiteCustomListPicker + | typeof NetSuiteCustomFieldMappingPicker + | typeof NetSuiteMenuWithTopDescriptionForm; type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country' | 'reportFields' | 'disabledListValues'; type ValueTypeMap = { @@ -126,6 +134,9 @@ type FormProps = { /** Whether ScrollWithContext should be used instead of regular ScrollView. Set to true when there's a nested Picker component in Form. */ scrollContextEnabled?: boolean; + /** Whether to use ScrollView */ + shouldUseScrollView?: boolean; + /** Container styles */ style?: StyleProp; diff --git a/src/components/HybridAppMiddleware.tsx b/src/components/HybridAppMiddleware.tsx deleted file mode 100644 index 5c6934f4fc3d..000000000000 --- a/src/components/HybridAppMiddleware.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import {useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {NativeModules} from 'react-native'; -import useSplashScreen from '@hooks/useSplashScreen'; -import BootSplash from '@libs/BootSplash'; -import Log from '@libs/Log'; -import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import * as Welcome from '@userActions/Welcome'; -import CONST from '@src/CONST'; -import type {Route} from '@src/ROUTES'; - -type HybridAppMiddlewareProps = { - children: React.ReactNode; -}; - -type HybridAppMiddlewareContextType = { - navigateToExitUrl: (exitUrl: Route) => void; - showSplashScreenOnNextStart: () => void; -}; -const HybridAppMiddlewareContext = React.createContext({ - navigateToExitUrl: () => {}, - showSplashScreenOnNextStart: () => {}, -}); - -/* - * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. - * It is crucial to make transitions between OldDot and NewDot look smooth. - */ -function HybridAppMiddleware(props: HybridAppMiddlewareProps) { - const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); - const [startedTransition, setStartedTransition] = useState(false); - const [finishedTransition, setFinishedTransition] = useState(false); - const navigation = useNavigation>(); - - /* - * Handles navigation during transition from OldDot. For ordinary NewDot app it is just pure navigation. - */ - const navigateToExitUrl = useCallback((exitUrl: Route) => { - if (NativeModules.HybridAppModule) { - setStartedTransition(true); - Log.info(`[HybridApp] Started transition to ${exitUrl}`, true); - } - - Navigation.navigate(exitUrl); - }, []); - - /** - * This function only affects iOS. If during a single app lifecycle we frequently transition from OldDot to NewDot, - * we need to artificially show the bootsplash because the app is only booted once. - */ - const showSplashScreenOnNextStart = useCallback(() => { - setIsSplashHidden(false); - setStartedTransition(false); - setFinishedTransition(false); - }, [setIsSplashHidden]); - - useEffect(() => { - if (!finishedTransition || isSplashHidden) { - return; - } - - Log.info('[HybridApp] Finished transtion', true); - BootSplash.hide().then(() => { - setIsSplashHidden(true); - Log.info('[HybridApp] Handling onboarding flow', true); - Welcome.handleHybridAppOnboarding(); - }); - }, [finishedTransition, isSplashHidden, setIsSplashHidden]); - - useEffect(() => { - if (!startedTransition) { - return; - } - - // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout. - const timeout = setTimeout(() => { - setFinishedTransition(true); - }, CONST.SCREEN_TRANSITION_END_TIMEOUT); - - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { - clearTimeout(timeout); - setFinishedTransition(true); - }); - - return () => { - clearTimeout(timeout); - unsubscribeTransitionEnd(); - }; - }, [navigation, startedTransition]); - - const contextValue = useMemo( - () => ({ - navigateToExitUrl, - showSplashScreenOnNextStart, - }), - [navigateToExitUrl, showSplashScreenOnNextStart], - ); - - return {props.children}; -} - -HybridAppMiddleware.displayName = 'HybridAppMiddleware'; - -export default HybridAppMiddleware; -export type {HybridAppMiddlewareContextType}; -export {HybridAppMiddlewareContext}; diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx new file mode 100644 index 000000000000..5b06e5626c6e --- /dev/null +++ b/src/components/HybridAppMiddleware/index.ios.tsx @@ -0,0 +1,130 @@ +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; +import {NativeEventEmitter, NativeModules} from 'react-native'; +import type {NativeModule} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; +import useSplashScreen from '@hooks/useSplashScreen'; +import BootSplash from '@libs/BootSplash'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SessionUtils from '@libs/SessionUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; + +type HybridAppMiddlewareProps = { + authenticated: boolean; + children: React.ReactNode; +}; + +/* + * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. + * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. + */ +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { + const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); + const [startedTransition, setStartedTransition] = useState(false); + const [finishedTransition, setFinishedTransition] = useState(false); + + const initialURL = useContext(InitialURLContext); + const exitToParam = useExitTo(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + + // In iOS, the HybridApp defines the `onReturnToOldDot` event. + // If we frequently transition from OldDot to NewDot during a single app lifecycle, + // we need to artificially display the bootsplash since the app is booted only once. + // Therefore, isSplashHidden needs to be updated at the appropriate time. + useEffect(() => { + if (!NativeModules.HybridAppModule) { + return; + } + const HybridAppEvents = new NativeEventEmitter(NativeModules.HybridAppModule as unknown as NativeModule); + const listener = HybridAppEvents.addListener(CONST.EVENTS.ON_RETURN_TO_OLD_DOT, () => { + Log.info('[HybridApp] `onReturnToOldDot` event received. Resetting state of HybridAppMiddleware', true); + setIsSplashHidden(false); + setStartedTransition(false); + setFinishedTransition(false); + setExitTo(undefined); + }); + + return () => { + listener.remove(); + }; + }, [setIsSplashHidden]); + + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, exitToParam]); + + useEffect(() => { + if (!startedTransition || finishedTransition) { + return; + } + + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + // We need to wait with navigating to exitTo until all login-related actions are complete. + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { + return; + } + + if (exitTo) { + Navigation.isNavigationReady().then(() => { + // We need to remove /transition from route history. + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); + + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } + + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); + + return children; +} + +HybridAppMiddleware.displayName = 'HybridAppMiddleware'; + +export default HybridAppMiddleware; diff --git a/src/components/HybridAppMiddleware/index.tsx b/src/components/HybridAppMiddleware/index.tsx new file mode 100644 index 000000000000..b8c72d9200ac --- /dev/null +++ b/src/components/HybridAppMiddleware/index.tsx @@ -0,0 +1,107 @@ +import type React from 'react'; +import {useContext, useEffect, useState} from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import {InitialURLContext} from '@components/InitialURLContextProvider'; +import useExitTo from '@hooks/useExitTo'; +import useSplashScreen from '@hooks/useSplashScreen'; +import BootSplash from '@libs/BootSplash'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as SessionUtils from '@libs/SessionUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {HybridAppRoute, Route} from '@src/ROUTES'; + +type HybridAppMiddlewareProps = { + authenticated: boolean; + children: React.ReactNode; +}; + +/* + * HybridAppMiddleware is responsible for handling BootSplash visibility correctly. + * It is crucial to make transitions between OldDot and NewDot look smooth. + * The middleware assumes that the entry point for HybridApp is the /transition route. + */ +function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) { + const {isSplashHidden, setIsSplashHidden} = useSplashScreen(); + const [startedTransition, setStartedTransition] = useState(false); + const [finishedTransition, setFinishedTransition] = useState(false); + + const initialURL = useContext(InitialURLContext); + const exitToParam = useExitTo(); + const [exitTo, setExitTo] = useState(); + + const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false}); + const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); + + // Save `exitTo` when we reach /transition route. + // `exitTo` should always exist during OldDot -> NewDot transitions. + useEffect(() => { + if (!NativeModules.HybridAppModule || !exitToParam || exitTo) { + return; + } + + Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam}); + setExitTo(exitToParam); + + Log.info(`[HybridApp] Started transition`, true); + setStartedTransition(true); + }, [exitTo, exitToParam]); + + useEffect(() => { + if (!startedTransition || finishedTransition) { + return; + } + + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL; + const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); + + // We need to wait with navigating to exitTo until all login-related actions are complete. + if (!authenticated || isLoggingInAsNewUser || isAccountLoading) { + return; + } + + if (exitTo) { + Navigation.isNavigationReady().then(() => { + // We need to remove /transition from route history. + // `useExitTo` returns undefined for routes other than /transition. + if (exitToParam) { + Log.info('[HybridApp] Removing /transition route from history', true); + Navigation.goBack(); + } + + Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo}); + Navigation.navigate(Navigation.parseHybridAppUrl(exitTo)); + setExitTo(undefined); + + setTimeout(() => { + Log.info('[HybridApp] Setting `finishedTransition` to true', true); + setFinishedTransition(true); + }, CONST.SCREEN_TRANSITION_END_TIMEOUT); + }); + } + }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]); + + useEffect(() => { + if (!finishedTransition || isSplashHidden) { + return; + } + + Log.info('[HybridApp] Finished transition, hiding BootSplash', true); + BootSplash.hide().then(() => { + setIsSplashHidden(true); + if (authenticated) { + Log.info('[HybridApp] Handling onboarding flow', true); + Welcome.handleHybridAppOnboarding(); + } + }); + }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]); + + return children; +} + +HybridAppMiddleware.displayName = 'HybridAppMiddleware'; + +export default HybridAppMiddleware; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index bd0824372799..7a8186d2f38e 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -8,6 +8,7 @@ import ConciergeExclamation from '@assets/images/product-illustrations/concierge import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg'; import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg'; import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; +import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg'; import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg'; import Hands from '@assets/images/product-illustrations/home-illustration-hands.svg'; import InvoiceOrange from '@assets/images/product-illustrations/invoice--orange.svg'; @@ -92,6 +93,7 @@ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustrati import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; +import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg'; import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg'; import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg'; import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg'; @@ -196,4 +198,6 @@ export { CheckmarkCircle, CreditCardEyes, LockClosedOrange, + FolderWithPapers, + VirtualCard, }; diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index a520693cff57..8c980838b841 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -74,8 +74,11 @@ type ReceiptImageProps = ( /** The size of the fallback icon */ fallbackIconSize?: number; - /** The colod of the fallback icon */ + /** The color of the fallback icon */ fallbackIconColor?: string; + + /** The background color of fallback icon */ + fallbackIconBackground?: string; }; function ReceiptImage({ @@ -93,6 +96,7 @@ function ReceiptImage({ fallbackIconSize, shouldUseInitialObjectPosition = false, fallbackIconColor, + fallbackIconBackground, }: ReceiptImageProps) { const styles = useThemeStyles(); @@ -129,6 +133,7 @@ function ReceiptImage({ fallbackIcon={fallbackIcon} fallbackIconSize={fallbackIconSize} fallbackIconColor={fallbackIconColor} + fallbackIconBackground={fallbackIconBackground} objectPosition={shouldUseInitialObjectPosition ? CONST.IMAGE_OBJECT_POSITION.INITIAL : CONST.IMAGE_OBJECT_POSITION.TOP} /> ); diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx new file mode 100644 index 000000000000..3911780d3965 --- /dev/null +++ b/src/components/Search/SearchContext.tsx @@ -0,0 +1,58 @@ +import React, {useCallback, useContext, useMemo, useState} from 'react'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type {SearchContext} from './types'; + +const defaultSearchContext = { + currentSearchHash: -1, + selectedTransactionIDs: [], + setCurrentSearchHash: () => {}, + setSelectedTransactionIds: () => {}, +}; + +const Context = React.createContext(defaultSearchContext); + +function SearchContextProvider({children}: ChildrenProps) { + const [searchContextData, setSearchContextData] = useState>({ + currentSearchHash: defaultSearchContext.currentSearchHash, + selectedTransactionIDs: defaultSearchContext.selectedTransactionIDs, + }); + + const setCurrentSearchHash = useCallback( + (searchHash: number) => { + setSearchContextData({ + ...searchContextData, + currentSearchHash: searchHash, + }); + }, + [searchContextData], + ); + + const setSelectedTransactionIds = useCallback( + (selectedTransactionIDs: string[]) => { + setSearchContextData({ + ...searchContextData, + selectedTransactionIDs, + }); + }, + [searchContextData], + ); + + const searchContext = useMemo( + () => ({ + ...searchContextData, + setCurrentSearchHash, + setSelectedTransactionIds, + }), + [searchContextData, setCurrentSearchHash, setSelectedTransactionIds], + ); + + return {children}; +} + +function useSearchContext() { + return useContext(Context); +} + +SearchContextProvider.displayName = 'SearchContextProvider'; + +export {SearchContextProvider, useSearchContext}; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 8445cb3bc72e..fc5c23d5c9ec 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -13,7 +13,6 @@ import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import * as ReportUtils from '@libs/ReportUtils'; -import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import * as SearchUtils from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; @@ -25,8 +24,10 @@ import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; import type {SearchDataTypes, SearchQuery} from '@src/types/onyx/SearchResults'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import {useSearchContext} from './SearchContext'; import SearchListWithHeader from './SearchListWithHeader'; import SearchPageHeader from './SearchPageHeader'; +import type {SearchColumnType, SortOrder} from './types'; type SearchProps = { query: SearchQuery; @@ -47,6 +48,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isLargeScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); + const {setCurrentSearchHash} = useSearchContext(); const getItemHeight = useCallback( (item: TransactionListItemType | ReportListItemType) => { @@ -83,6 +85,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { return; } + setCurrentSearchHash(hash); SearchActions.search({hash, query, policyIDs, offset: 0, sortBy, sortOrder}); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [hash, isOffline]); @@ -197,7 +200,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { getItemHeight={getItemHeight} shouldDebounceRowSelect shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + listHeaderWrapperStyle={[styles.ph8, styles.pv3, styles.pb5]} containerStyle={[styles.pv0]} showScrollIndicator={false} onEndReachedThreshold={0.75} diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 3ebc2797947a..cff74fe08a0a 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -1,3 +1,6 @@ +import type {ValueOf} from 'react-native-gesture-handler/lib/typescript/typeUtils'; +import type CONST from '@src/CONST'; + /** Model of the selected transaction */ type SelectedTransactionInfo = { /** Whether the transaction is selected */ @@ -13,5 +16,14 @@ type SelectedTransactionInfo = { /** Model of selected results */ type SelectedTransactions = Record; -// eslint-disable-next-line import/prefer-default-export -export type {SelectedTransactionInfo, SelectedTransactions}; +type SortOrder = ValueOf; +type SearchColumnType = ValueOf; + +type SearchContext = { + currentSearchHash: number; + selectedTransactionIDs: string[]; + setCurrentSearchHash: (hash: number) => void; + setSelectedTransactionIds: (selectedTransactionIds: string[]) => void; +}; + +export type {SelectedTransactionInfo, SelectedTransactions, SearchColumnType, SortOrder, SearchContext}; diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 5af3d84bf32f..ad77070c1b99 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -1,46 +1,78 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; import Badge from '@components/Badge'; import Button from '@components/Button'; import * as Expensicons from '@components/Icon/Expensicons'; +import {useSearchContext} from '@components/Search/SearchContext'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; +import * as SearchActions from '@userActions/Search'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ROUTES from '@src/ROUTES'; +import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; + +const actionTranslationsMap: Record = { + view: 'common.view', + review: 'common.review', + done: 'common.done', + paid: 'iou.settledExpensify', + hold: 'iou.hold', + unhold: 'iou.unhold', +}; type ActionCellProps = { - onButtonPress: () => void; - action?: string; + action?: SearchTransactionAction; + transactionID?: string; isLargeScreenWidth?: boolean; isSelected?: boolean; + goToItem: () => void; }; -function ActionCell({onButtonPress, action = CONST.SEARCH.ACTION_TYPES.VIEW, isLargeScreenWidth = true, isSelected = false}: ActionCellProps) { +function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); const theme = useTheme(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + + const {currentSearchHash} = useSearchContext(); + + const onButtonPress = useCallback(() => { + if (!transactionID) { + return; + } + + if (action === CONST.SEARCH.ACTION_TYPES.HOLD) { + Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(CONST.SEARCH.TAB.ALL, transactionID)); + } else if (action === CONST.SEARCH.ACTION_TYPES.UNHOLD) { + SearchActions.unholdMoneyRequestOnSearch(currentSearchHash, [transactionID]); + } + }, [action, currentSearchHash, transactionID]); + if (!isLargeScreenWidth) { return null; } + const text = translate(actionTranslationsMap[action]); + if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) { - const buttonTextKey = action === CONST.SEARCH.ACTION_TYPES.PAID ? 'iou.settledExpensify' : 'common.done'; return ( + ); + } + return (