diff --git a/.gitignore b/.gitignore index 430962a17709..459d8b1d1271 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ android/app/src/main/java/com/expensify/chat/generated/ # Vscode .vscode +# Fleet +.fleet + # node.js # node_modules/ diff --git a/Mobile-Expensify b/Mobile-Expensify index 80f59d5dc24a..3753c8573db7 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 80f59d5dc24a2159897f814c8017eff313970873 +Subproject commit 3753c8573db70be4141a399bdfab466774f1a002 diff --git a/__mocks__/Illustrations.ts b/__mocks__/Illustrations.ts new file mode 100644 index 000000000000..8b2b033a8ff8 --- /dev/null +++ b/__mocks__/Illustrations.ts @@ -0,0 +1,307 @@ +const Abracadabra = 'Abracadabra'; +const BankArrowPink = 'BankArrowPink'; +const BankMouseGreen = 'BankMouseGreen'; +const BankUserGreen = 'BankUserGreen'; +const BigRocket = 'BigRocket'; +const BrokenMagnifyingGlass = 'BrokenMagnifyingGlass'; +const ChatBubbles = 'ChatBubbles'; +const CoffeeMug = 'CoffeeMug'; +const ConciergeBlue = 'ConciergeBlue'; +const ConciergeExclamation = 'ConciergeExclamation'; +const CreditCardsBlue = 'CreditCardsBlue'; +const EmailAddress = 'EmailAddress'; +const EmptyCardState = 'EmptyCardState'; +const EmptyStateExpenses = 'EmptyStateExpenses'; +const EnvelopeReceipt = 'EnvelopeReceipt'; +const FolderOpen = 'FolderOpen'; +const HandCard = 'HandCard'; +const HotDogStand = 'HotDogStand'; +const InvoiceOrange = 'InvoiceOrange'; +const JewelBoxBlue = 'JewelBoxBlue'; +const JewelBoxGreen = 'JewelBoxGreen'; +const PaymentHands = 'PaymentHands'; +const JewelBoxPink = 'JewelBoxPink'; +const JewelBoxYellow = 'JewelBoxYellow'; +const MagicCode = 'MagicCode'; +const Mailbox = 'Mailbox'; +const MoneyEnvelopeBlue = 'MoneyEnvelopeBlue'; +const MoneyMousePink = 'MoneyMousePink'; +const MushroomTopHat = 'MushroomTopHat'; +const ReceiptsSearchYellow = 'ReceiptsSearchYellow'; +const ReceiptYellow = 'ReceiptYellow'; +const ReceiptWrangler = 'ReceiptWrangler'; +const RocketBlue = 'RocketBlue'; +const RocketOrange = 'RocketOrange'; +const SanFrancisco = 'SanFrancisco'; +const SafeBlue = 'SafeBlue'; +const SmallRocket = 'SmallRocket'; +const TadaYellow = 'TadaYellow'; +const TadaBlue = 'TadaBlue'; +const ToddBehindCloud = 'ToddBehindCloud'; +const ToddWithPhones = 'ToddWithPhones'; +const GpsTrackOrange = 'GpsTrackOrange'; +const ShieldYellow = 'ShieldYellow'; +const MoneyReceipts = 'MoneyReceipts'; +const PinkBill = 'PinkBill'; +const CreditCardsNew = 'CreditCardsNew'; +const CreditCardsNewGreen = 'CreditCardsNewGreen'; +const InvoiceBlue = 'InvoiceBlue'; +const LaptopwithSecondScreenandHourglass = 'LaptopwithSecondScreenandHourglass'; +const LockOpen = 'LockOpen'; +const Luggage = 'Luggage'; +const MoneyIntoWallet = 'MoneyIntoWallet'; +const MoneyWings = 'MoneyWings'; +const OpenSafe = 'OpenSafe'; +const TrackShoe = 'TrackShoe'; +const BankArrow = 'BankArrow'; +const ConciergeBubble = 'ConciergeBubble'; +const ConciergeNew = 'ConciergeNew'; +const MoneyBadge = 'MoneyBadge'; +const TreasureChest = 'TreasureChest'; +const ThumbsUpStars = 'ThumbsUpStars'; +const Hands = 'Hands'; +const HandEarth = 'HandEarth'; +const SmartScan = 'SmartScan'; +const Hourglass = 'Hourglass'; +const CommentBubbles = 'CommentBubbles'; +const CommentBubblesBlue = 'CommentBubblesBlue'; +const TrashCan = 'TrashCan'; +const TeleScope = 'TeleScope'; +const Profile = 'Profile'; +const Puzzle = 'Puzzle'; +const PalmTree = 'PalmTree'; +const LockClosed = 'LockClosed'; +const Gears = 'Gears'; +const QRCode = 'QRCode'; +const RealtimeReport = 'RealtimeReport'; +const HoldExpense = 'HoldExpense'; +const ReceiptEnvelope = 'ReceiptEnvelope'; +const Approval = 'Approval'; +const WalletAlt = 'WalletAlt'; +const Workflows = 'Workflows'; +const PendingBank = 'PendingBank'; +const ThreeLeggedLaptopWoman = 'ThreeLeggedLaptopWoman'; +const House = 'House'; +const Building = 'Building'; +const Buildings = 'Buildings'; +const Alert = 'Alert'; +const TeachersUnite = 'TeachersUnite'; +const Abacus = 'Abacus'; +const Binoculars = 'Binoculars'; +const CompanyCard = 'CompanyCard'; +const ReceiptUpload = 'ReceiptUpload'; +const ExpensifyCardIllustration = 'ExpensifyCardIllustration'; +const SplitBill = 'SplitBill'; +const PiggyBank = 'PiggyBank'; +const Pillow = 'Pillow'; +const Accounting = 'Accounting'; +const Car = 'Car'; +const Coins = 'Coins'; +const Pencil = 'Pencil'; +const Tag = 'Tag'; +const CarIce = 'CarIce'; +const ReceiptLocationMarker = 'ReceiptLocationMarker'; +const Lightbulb = 'Lightbulb'; +const EmptyStateTravel = 'EmptyStateTravel'; +const Stopwatch = 'Stopwatch'; +const SubscriptionAnnual = 'SubscriptionAnnual'; +const SubscriptionPPU = 'SubscriptionPPU'; +const ExpensifyApprovedLogo = 'ExpensifyApprovedLogo'; +const ExpensifyApprovedLogoLight = 'ExpensifyApprovedLogoLight'; +const SendMoney = 'SendMoney'; +const CheckmarkCircle = 'CheckmarkCircle'; +const CreditCardEyes = 'CreditCardEyes'; +const LockClosedOrange = 'LockClosedOrange'; +const EmptyState = 'EmptyState'; +const FolderWithPapers = 'FolderWithPapers'; +const VirtualCard = 'VirtualCard'; +const Tire = 'Tire'; +const BigVault = 'BigVault'; +const Filters = 'Filters'; +const MagnifyingGlassMoney = 'MagnifyingGlassMoney'; +const Rules = 'Rules'; +const CompanyCardsEmptyState = 'CompanyCardsEmptyState'; +const AmexCompanyCards = 'AmexCompanyCards'; +const MasterCardCompanyCards = 'MasterCardCompanyCards'; +const VisaCompanyCards = 'VisaCompanyCards'; +const CompanyCardsPendingState = 'CompanyCardsPendingState'; +const VisaCompanyCardDetail = 'VisaCompanyCardDetail'; +const MasterCardCompanyCardDetail = 'MasterCardCompanyCardDetail'; +const AmexCardCompanyCardDetail = 'AmexCardCompanyCardDetail'; +const TurtleInShell = 'TurtleInShell'; +const BankOfAmericaCompanyCardDetail = 'BankOfAmericaCompanyCardDetail'; +const BrexCompanyCardDetail = 'BrexCompanyCardDetail'; +const CapitalOneCompanyCardDetail = 'CapitalOneCompanyCardDetail'; +const ChaseCompanyCardDetail = 'ChaseCompanyCardDetail'; +const CitibankCompanyCardDetail = 'CitibankCompanyCardDetail'; +const StripeCompanyCardDetail = 'StripeCompanyCardDetail'; +const WellsFargoCompanyCardDetail = 'WellsFargoCompanyCardDetail'; +const PerDiem = 'PerDiem'; +const AmexCardCompanyCardDetailLarge = 'AmexCardCompanyCardDetailLarge'; +const BankOfAmericaCompanyCardDetailLarge = 'BankOfAmericaCompanyCardDetailLarge'; +const BrexCompanyCardDetailLarge = 'BrexCompanyCardDetailLarge'; +const CapitalOneCompanyCardDetailLarge = 'CapitalOneCompanyCardDetailLarge'; +const ChaseCompanyCardDetailLarge = 'ChaseCompanyCardDetailLarge'; +const CitibankCompanyCardDetailLarge = 'CitibankCompanyCardDetailLarge'; +const MasterCardCompanyCardDetailLarge = 'MasterCardCompanyCardDetailLarge'; +const StripeCompanyCardDetailLarge = 'StripeCompanyCardDetailLarge'; +const VisaCompanyCardDetailLarge = 'VisaCompanyCardDetailLarge'; +const WellsFargoCompanyCardDetailLarge = 'WellsFargoCompanyCardDetailLarge'; +const Flash = 'Flash'; +const ExpensifyMobileApp = 'ExpensifyMobileApp'; +const ReportReceipt = 'ReportReceipt'; + +export { + Abracadabra, + BankArrowPink, + BankMouseGreen, + BankUserGreen, + BigRocket, + BrokenMagnifyingGlass, + ChatBubbles, + CoffeeMug, + ConciergeBlue, + ConciergeExclamation, + CreditCardsBlue, + EmailAddress, + EmptyCardState, + EmptyStateExpenses, + EnvelopeReceipt, + FolderOpen, + HandCard, + HotDogStand, + InvoiceOrange, + JewelBoxBlue, + JewelBoxGreen, + PaymentHands, + JewelBoxPink, + JewelBoxYellow, + MagicCode, + Mailbox, + MoneyEnvelopeBlue, + MoneyMousePink, + MushroomTopHat, + ReceiptsSearchYellow, + ReceiptYellow, + ReceiptWrangler, + RocketBlue, + RocketOrange, + SanFrancisco, + SafeBlue, + SmallRocket, + TadaYellow, + TadaBlue, + ToddBehindCloud, + ToddWithPhones, + GpsTrackOrange, + ShieldYellow, + MoneyReceipts, + PinkBill, + CreditCardsNew, + CreditCardsNewGreen, + InvoiceBlue, + LaptopwithSecondScreenandHourglass, + LockOpen, + Luggage, + MoneyIntoWallet, + MoneyWings, + OpenSafe, + TrackShoe, + BankArrow, + ConciergeBubble, + ConciergeNew, + MoneyBadge, + TreasureChest, + ThumbsUpStars, + Hands, + HandEarth, + SmartScan, + Hourglass, + CommentBubbles, + CommentBubblesBlue, + TrashCan, + TeleScope, + Profile, + Puzzle, + PalmTree, + LockClosed, + Gears, + QRCode, + RealtimeReport, + HoldExpense, + ReceiptEnvelope, + Approval, + WalletAlt, + Workflows, + PendingBank, + ThreeLeggedLaptopWoman, + House, + Building, + Buildings, + Alert, + TeachersUnite, + Abacus, + Binoculars, + CompanyCard, + ReceiptUpload, + ExpensifyCardIllustration, + SplitBill, + PiggyBank, + Pillow, + Accounting, + Car, + Coins, + Pencil, + Tag, + CarIce, + ReceiptLocationMarker, + Lightbulb, + EmptyStateTravel, + Stopwatch, + SubscriptionAnnual, + SubscriptionPPU, + ExpensifyApprovedLogo, + ExpensifyApprovedLogoLight, + SendMoney, + CheckmarkCircle, + CreditCardEyes, + LockClosedOrange, + EmptyState, + FolderWithPapers, + VirtualCard, + Tire, + BigVault, + Filters, + MagnifyingGlassMoney, + Rules, + CompanyCardsEmptyState, + AmexCompanyCards, + MasterCardCompanyCards, + VisaCompanyCards, + CompanyCardsPendingState, + VisaCompanyCardDetail, + MasterCardCompanyCardDetail, + AmexCardCompanyCardDetail, + TurtleInShell, + BankOfAmericaCompanyCardDetail, + BrexCompanyCardDetail, + CapitalOneCompanyCardDetail, + ChaseCompanyCardDetail, + CitibankCompanyCardDetail, + StripeCompanyCardDetail, + WellsFargoCompanyCardDetail, + PerDiem, + AmexCardCompanyCardDetailLarge, + BankOfAmericaCompanyCardDetailLarge, + BrexCompanyCardDetailLarge, + CapitalOneCompanyCardDetailLarge, + ChaseCompanyCardDetailLarge, + CitibankCompanyCardDetailLarge, + MasterCardCompanyCardDetailLarge, + StripeCompanyCardDetailLarge, + VisaCompanyCardDetailLarge, + WellsFargoCompanyCardDetailLarge, + Flash, + ExpensifyMobileApp, + ReportReceipt, +}; diff --git a/android/app/build.gradle b/android/app/build.gradle index 49fcdbb4fa68..7e71c40d7fab 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009010600 - versionName "9.1.6-0" + versionCode 1009010700 + versionName "9.1.7-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/assets/images/companyCards/card=-generic.svg b/assets/images/companyCards/card=-generic.svg deleted file mode 100644 index 192c194da9e7..000000000000 --- a/assets/images/companyCards/card=-generic.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/companyCards/generic-csv-dark.svg b/assets/images/companyCards/generic-csv-dark.svg new file mode 100644 index 000000000000..0e450b8db437 --- /dev/null +++ b/assets/images/companyCards/generic-csv-dark.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/generic-csv-light.svg b/assets/images/companyCards/generic-csv-light.svg new file mode 100644 index 000000000000..0075d5a35f79 --- /dev/null +++ b/assets/images/companyCards/generic-csv-light.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/generic-dark.svg b/assets/images/companyCards/generic-dark.svg new file mode 100644 index 000000000000..58ee197a1107 --- /dev/null +++ b/assets/images/companyCards/generic-dark.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/generic-light.svg b/assets/images/companyCards/generic-light.svg new file mode 100644 index 000000000000..e7e8d192c74f --- /dev/null +++ b/assets/images/companyCards/generic-light.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/card-generic-large.svg b/assets/images/companyCards/large/card-generic-large.svg deleted file mode 100644 index 0979107526e6..000000000000 --- a/assets/images/companyCards/large/card-generic-large.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/companyCards/large/generic-csv-dark-large.svg b/assets/images/companyCards/large/generic-csv-dark-large.svg new file mode 100644 index 000000000000..6afc916636bc --- /dev/null +++ b/assets/images/companyCards/large/generic-csv-dark-large.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/generic-csv-light-large.svg b/assets/images/companyCards/large/generic-csv-light-large.svg new file mode 100644 index 000000000000..6d60cffc1c61 --- /dev/null +++ b/assets/images/companyCards/large/generic-csv-light-large.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/generic-dark-large.svg b/assets/images/companyCards/large/generic-dark-large.svg new file mode 100644 index 000000000000..de2fd514569e --- /dev/null +++ b/assets/images/companyCards/large/generic-dark-large.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/companyCards/large/generic-light-large.svg b/assets/images/companyCards/large/generic-light-large.svg new file mode 100644 index 000000000000..b5559951d34e --- /dev/null +++ b/assets/images/companyCards/large/generic-light-large.svg @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/docs/articles/expensify-classic/domains/Create-A-Group.md b/docs/articles/expensify-classic/domains/Create-A-Group.md deleted file mode 100644 index fb70faffa27e..000000000000 --- a/docs/articles/expensify-classic/domains/Create-A-Group.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Create a group -description: How to set different rules for different members of your domain ---- -
- -To set different domain rules for different members, you can place them into groups. For example, many organizations create different groups for employees and managers since they generally need different domain permissions. - -To create a group, - -1. Hover over Settings, then click **Domains**. -2. Click the name of the domain. -3. Click the **Groups** tab on the left. -4. Click **Create Group**. -5. Select all of the group settings and permissions. - - **Permission Group Name**: Enter a name for the group - - **Default Group**: Determine if new domain members will be automatically added to this group. - - **Strictly enforce expense workspace rules**: Determine if all expense rules must be met before people in this group can submit a report. - - **Restrict primary login selection**: Determine if members of this group will be restricted from using a personal email address to access their Expensify account. - - **Restrict expense workspace creation/removal**: Determine if members of this group will be allowed to create new workspaces. - - **Preferred workspace**: Determine if this group will automatically have their expenses and reports posted to a specific workspace. - - **Set preferred workspace to**: If preferred workspace is enabled, select which workspace members of this group will have set as their preferred workspace. - - **Expensify Card Preferred Workspace**: If preferred workspace is enabled, determine if Expensify Card transactions for this group will be posted to the preferred workspace listed for the Expensify Card instead of the preferred workspace listed in the above settings. -6. Click **Save**. - -
\ No newline at end of file diff --git a/docs/articles/expensify-classic/domains/Domain-Groups.md b/docs/articles/expensify-classic/domains/Domain-Groups.md new file mode 100644 index 000000000000..97f67427ece3 --- /dev/null +++ b/docs/articles/expensify-classic/domains/Domain-Groups.md @@ -0,0 +1,42 @@ +--- +title: Domain Groups +description: How to set different rules for different members of your domain +--- + +To set different domain rules for different members, you can place them into groups. This allows organizations to customize permissions based on roles, such as employees and managers, ensuring they have the appropriate access and settings. + +--- +# Configuring Domain Groups + +1. Hover over **Settings**, then click **Domains**. +2. Click the name of the domain. +3. Click the **Groups** tab. +4. Click **Create Group**. +5. Configure the group settings and permissions: + - **Permission Group Name**: Enter a name for the group. + - **Default Group**: Choose if new domain members will be automatically added to this group. + - **Strictly enforce expense workspace rules**: Decide if all expense rules must be met before group members can submit a report. + - **Restrict primary login selection**: Choose whether members can access their Expensify account using a personal email. + - **Restrict expense workspace creation/removal**: Set whether members can create or remove workspaces. + - **Preferred workspace**: Select a default workspace for group members' expenses and reports. + - **Set preferred workspace to**: If enabled, specify which workspace will be set as preferred. + - **Expensify Card Preferred Workspace**: If the preferred workspace is enabled, check whether Expensify Card transactions for this group will be posted to the preferred workspace for the Expensify Card instead of the one in the above settings. +6. Click **Save**. + +--- +# When to Use Different Domain Group Permissions + +## Strictly enforce expense workspace rules +This setting ensures all workspace-level rules are followed before an expense report is submitted. This prevents expense reports from being submitted with missing receipts, incorrect categories, or violations. + +## Restrict primary login selection +Enable this setting to ensure employees use their company email instead of a personal email account when submitting expense reports. This helps to maintain security and compliance within your organization. + +## Restrict expense workspace creation/removal +Set this to prevent employees from creating additional workspaces outside of the company workspace they're already a member of. This will ensure expense reports are always routed through the correct company-approved channels. + +## Preferred workspace +If you have multiple workspaces across several teams, use this setting to assign an employee to their corresponding workspace. For instance, use this for sales teams so their expenses automatically post to the "Sales Department" workspace, reducing the need for manual adjustments. + +## Expensify Card Preferred Workspace +Enable this if your team uses the Expensify Cards for business expenses. This will ensure that all transactions are posted directly to the correct workspace without additional setup. diff --git a/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md b/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md deleted file mode 100644 index e83640403ce4..000000000000 --- a/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Create Expense Rules -description: Automatically categorize, tag, and report expenses based on the merchant's name ---- - -Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name. - -# Create expense rules - -1. Hover over **Settings** and click **Account**. -2. Click **Expense Rules**. -2. Click **New Rule**. -3. Add what the merchant name should contain in order for the rule to be applied. *Note: If you enter just a period, the rule will apply to all expenses regardless of the merchant name. Universal Rules will always take precedence over all other expense rules.* -4. Choose from the following rules: -- **Merchant:** Updates the merchant name (e.g., “Starbucks #238” could be changed to “Starbucks”) -- **Category:** Applies a workspace category to the expense -- **Tag:** Applies a tag to the expense (e.g., a Department or Location) -- **Description:** Adds a description to the description field on the expense -- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable -- **Billable**: Determines whether the expense is billable -- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created if the "Create report if necessary" checkbox is selected. - -![Fields to create a new expense rule, including the characters a merchant's name should contain for the rule to apply, as well as what changes should be applied to the expense including the merchant name, category, tag, description, reimbursability, whether it is billable, and what report it will be added to.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} - -{:start="6"} -6. (Optional) To apply the rule to previously entered expenses, select the **Apply to existing matching expenses** checkbox. You can also click **Preview Matching Expenses** to see if your rule matches the intended expenses. - -# How rules are applied - -In general, your expense rules will be applied in order, from **top to bottom**, (i.e., from the first rule). However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied: - -1. A Universal Rule will **always** be applied over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule. -2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report. -3. If the expense is from a company card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules. -4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. - -# Create an expense rule from changes made to an expense - -If you open an expense and change it, you can then create an expense rule based on those changes by selecting the “Create a rule based on your changes" checkbox. *Note: The expense must be saved, reopened, and edited for this option to appear.* - -![The "Create a rule based on your changes" checkbox is located in the bottom right corner of the popup window, to the left of the Save button.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} - -# Delete an expense rule - -To delete an expense rule, - -1. Hover over **Settings** and click **Account**. -2. Click **Expense Rules**. -3. Scroll down to the rule you’d like to remove and click the trash can icon. - -![The Trash icon to delete an expense rule is located at the top right of the box containing the expense rule, to the left of the Edit icon.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} - -{% include faq-begin.md %} - -## How can I use expense rules to vendor match when exporting to an accounting package? - -When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. - -For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. - -{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expenses/Expense-Rules.md b/docs/articles/expensify-classic/expenses/Expense-Rules.md new file mode 100644 index 000000000000..190c0f70c486 --- /dev/null +++ b/docs/articles/expensify-classic/expenses/Expense-Rules.md @@ -0,0 +1,76 @@ +--- +title: Expense Rules +description: Automatically categorize, tag, and report expenses based on the merchant's name. +--- + +Expense rules in Expensify help automate the categorization, tagging, and reporting of expenses based on merchant names, reducing manual work. By setting up these rules at the account level, employees can streamline expense management and ensure consistency across reports. + +--- + +# Create an Expense Rule + +1. Hover over **Settings** and click **Account**. +2. Click **Expense Rules**. +3. Click **New Rule**. +4. In the **Merchant Name Contains** field, enter part of the merchant name that should trigger the rule. + - **Note:** If you enter only a period (`.`), the rule applies to all expenses. Universal Rules take precedence over all other expense rules. +5. Select the rules to apply when a matching expense is detected: + - **Merchant:** Standardizes the merchant name (e.g., "Starbucks #238" → "Starbucks"). + - **Category:** Assigns a workspace category to the expense. + - **Tag:** Adds a tag (e.g., Department or Location). + - **Description:** Updates the description field of the expense. + - **Reimbursability:** Marks the expense as reimbursable or non-reimbursable. + - **Billable:** Flags the expense as billable. + - **Add to a report named:** Assigns the expense to a specific report. If **Create report if necessary** is selected, a new one is created if the report does not exist. + +![Fields to create a new expense rule, including the characters a merchant's name should contain for the rule to apply, as well as what changes should be applied to the expense including the merchant name, category, tag, description, reimbursability, whether it is billable, and what report it will be added to.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} + + +6. (Optional) Select **Apply to existing matching expenses** to update past expenses. +7. Click **Preview Matching Expenses** to check if the rule applies correctly. + +--- + +# How Rules Are Applied + +Expense rules are processed from **top to bottom** in the list. However, other settings may override them. The rule hierarchy is: + +1. **Universal Rules** always override other expense category rules. +2. **Scheduled Submit** with **Enforce Default Report Title** enabled takes precedence over expense rule-based report assignments. +3. **Company Card Rules** for enforced workspaces take priority over individual expense rules. +4. **Accounting Integrations** may override expense rule settings when expenses are exported. + +--- + +# Create an Expense Rule from an Edited Expense + +If you modify an expense manually, you can create a rule based on those changes: + +1. Open the expense. +2. Make the necessary edits. +3. Select **Create a rule based on your changes** before saving. + - **Note:** The option appears only after saving, reopening, and editing an expense. + +![The "Create a rule based on your changes" checkbox is located in the bottom right corner of the popup window, to the left of the Save button.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} + +--- + +# Delete an Expense Rule + +1. Hover over **Settings** and click **Account**. +2. Click **Expense Rules**. +3. Find the rule you want to remove and click the **Trash** icon. + +![The Trash icon to delete an expense rule is located at the top right of the box containing the expense rule, to the left of the Edit icon.](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} + +--- + +# FAQ + +## How can I use expense rules for vendor matching in an accounting integration? + +When exporting non-reimbursable expenses, the **Payee** field in the accounting software will show "Credit Card Misc." if there is no exact match for the merchant name. This prevents multiple variations of the same vendor (e.g., "Starbucks" vs. "Starbucks #1234") from being created. + +To avoid this, use **Expense Rules** to standardize vendor names before export. +- **Supported integrations:** QuickBooks Online, QuickBooks Desktop, Xero. +- **Not supported for:** NetSuite, Sage Intacct (due to API limitations). diff --git a/docs/articles/expensify-classic/expenses/Expense-Types.md b/docs/articles/expensify-classic/expenses/Expense-Types.md index faf670469362..d42d2636db4a 100644 --- a/docs/articles/expensify-classic/expenses/Expense-Types.md +++ b/docs/articles/expensify-classic/expenses/Expense-Types.md @@ -1,43 +1,52 @@ --- title: Expense Types -description: Details of the different Expense filters and Expense Types +description: Learn how to organize reports by expense type and identify different expense categories in Expensify. --- -## Organize a Report by Expense Type -Organizing a report by expense type can make it easier to review expenses on a report. +Understanding expense types in Expensify helps you track and categorize business spending more effectively. This guide covers how to organize reports by expense type and explains the differences between reimbursable, non-reimbursable, and billable expenses. + +# Organize a Report by Expense Type + +Organizing reports by expense type helps streamline expense review: 1. Open the desired report. -2. Click Details in the upper right corner of the report. -3. Click the View dropdown and select Detailed. -4. Click the Split by dropdown and select Reimbursable or Billable. +2. Click **Details** in the upper-right corner. +3. Click the **View** dropdown and select **Detailed**. +4. Click the **Split by** dropdown and select **Reimbursable** or **Billable**. +5. To group expenses further, use the **Group by** dropdown to select **Category** or **Tags**. -To group the expenses by category or tag, you can also click the Group by dropdown and select Category or Tags. +--- -## Identify Expense Types -The right side of every report provides the total for all the expenses. Under the total, there is a breakdown of reimbursable, billable, and non-reimbursable amounts (depending on the expense types that exist on the report). +# Identify Expense Types +The right side of every report displays total expenses, broken down by **reimbursable**, **billable**, and **non-reimbursable** amounts. -- Reimbursable expenses: Expenses paid to the employee, including: - - Cash & personal card: Expenses paid for by the employee on behalf of the business. - - Per diem: Expenses for a daily or partial daily rate [configured in your Workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses). - - Time: An hourly rate for your employees or jobs as [set for your workspace](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). This expense type is usually used by contractors or small businesses billing the customer via [Expensify Invoicing](https://help.expensify.com/articles/expensify-classic/workspaces/Set-Up-Invoicing). - - Distance: Expenses related to business travel. -- Non-reimbursable expenses: Expenses directly covered by the business, typically on company cards. -- Billable expenses: Business or employee expenses that must be billed to a specific client or vendor. This option is for tracking expenses for invoicing to customers, clients, or other departments. Any kind of expense can be billable, in _addition_ to being either reimbursable or non-reimbursable. +## Reimbursable Expenses +Expenses paid by employees on behalf of the business, including: +- **Cash & Personal Card:** Out-of-pocket business expenses. +- **Per Diem:** Daily expense allowances configured in your [workspace settings](https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses). +- **Time:** Hourly wages for jobs, typically used for contractor invoicing. Configure rates [here](https://help.expensify.com/articles/expensify-classic/workspaces/Set-time-and-distance-rates). +- **Distance:** Mileage-related expenses. -![Image of a report showing multiple expense totals]({{site.url}}/assets/images/amounts.png){:width="100%"} +## Non-Reimbursable Expenses +Expenses that are directly covered by the business, usually on company cards. -{% include faq-begin.md %} +## Billable Expenses +Expenses billed to a client or vendor. Any expense—reimbursable or non-reimbursable—can also be billable. -**What’s the difference between an expense, a receipt, and a report attachment?** +![Image of a report showing multiple expense totals]({{site.url}}/assets/images/amounts.png){:width="100%"} -- **Expense:** Created when you SmartScan or manually upload a receipt from a purchase. -- **Receipt:** A picture file that is automatically attached to the expense during the SmartScan process. -- **Report Attachments:** Additional documents that need to be submitted to your approver (e.g., supplemental documents to the purchase) can be added to a report any time by clicking the paperclip icon in the comments at the bottom of the report. +--- -**How are credits or refunds displayed in Expensify?** +# FAQ -In Expensify, a credit is displayed as an expense with a minus in front of it (e.g., -$1.00). Expensify defaults all expenses as something that needs to be paid by the company. So a credit that is returned to the company is displayed as a negative expense. +## What’s the difference between an expense, a receipt, and a report attachment? +- **Expense:** Created when you SmartScan or manually upload a receipt. +- **Receipt:** Image file automatically attached to an expense via SmartScan. +- **Report Attachment:** Additional documents (e.g., supporting documents) added via the paperclip icon in report comments. -If a report includes a credit or a refund expense, it will offset the total amount on the report. For example, if the report has two reimbursable expenses, one for $400 and one for $500, then the total reimbursable amount is $900. Conversely, an expense for -$400 and one for $500 will be a total reimbursable amount of $500. +## How are credits or refunds displayed in Expensify? +Credits appear as **negative expenses** (e.g., -$1.00). They offset the total report amount. -{% include faq-end.md %} +For example: +- A report with **$400** and **$500** reimbursable expenses shows a total of **$900**. +- A report with **-$400** and **$500** expenses results in a **$100** total. diff --git a/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md b/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md new file mode 100644 index 000000000000..cd58d5d6b64d --- /dev/null +++ b/docs/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace.md @@ -0,0 +1,132 @@ +--- +title: Join Your Company's Workspace +description: Get started with Expensify as an employee or company member. +--- + +Getting started with Expensify is quick and easy, whether you're a new employee or an existing team member joining a company workspace. This guide walks you through downloading the app, setting up your profile, managing expenses, and securing your account. + +--- + +# 1. Download the Mobile App + +Upload expenses and check reports from your phone by downloading the Expensify mobile app. + +- [iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959) +- [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US) + +For a full walkthrough on creating and submitting expenses via the mobile app, click [here](https://expensify.navattic.com/fl150n1n)! + +--- + +# 2. Add Your Name and Photo + +**Desktop:** +1. Click your profile image in the main menu. +2. Hover over the profile picture and click **Change**. +3. Update your profile: + - **Name**: Enter your first and last name, then click **Update**. + - **Photo**: Click **Add Photo** to upload an image. + +**Mobile:** +1. Tap the ☰ menu icon in the top left. +2. Tap your profile picture. +3. Tap the **Edit** icon to update your name or photo. + - **Name**: Enter your first and/or last name, then tap **Update**. + - **Photo**: Tap **Upload Photo**, then: + - Tap the camera button to take a new photo. + - Tap the photo icon to select an existing image. + +--- + +# 3. Meet Concierge + +Concierge is your personal assistant, available on both desktop and mobile. It helps by: + +- Reminding you to submit expenses. +- Alerting you when more information is needed. +- Providing updates on new features. + +For support, click the green chat bubble at the bottom of the screen to chat with Concierge. + +--- + +# 4. Learn How to Add an Expense + +Employees can document reimbursable and non-reimbursable expenses using SmartScan or manual entry. + +## SmartScan a Receipt + +**Desktop:** +1. Click the **Expenses** tab. +2. Click the **+** icon and select **Scan Receipt**. +3. Upload a saved receipt image. + +**Mobile:** +1. Tap the camera icon in the bottom right. +2. Upload or take a receipt photo: + - **Upload**: Tap the photo icon and select an image. + - **Take a photo**: Ensure details are clear, then capture the image. + +*You can also email receipts to receipts@expensify.com from any email address associated with your Expensify account.* + +## Manually Enter an Expense + +Desktop: +1. Click the **Expenses** tab. +2. Click the **+** icon. +3. Select the expense type and enter details. +4. Click **Save**. + +Mobile: +1. Tap the ☰ menu icon and select **Expenses**. +2. Tap the **+** icon. +3. Select the expense type and enter details. +4. Tap **Save**. + +--- + +# 5. Create & Submit an Expense Report + +Your expenses may be automatically added to a report. If not, follow these steps to create and submit one. + +**Desktop:** +1. Click the **Reports** tab. +2. Click **New Report** > **Expense Report**. +3. Click **Add Expenses** and select expenses. +4. Click **Submit**, enter approver details, and click **Send**. + +**Mobile:** +1. Tap ☰ > **Reports**. +2. Tap the **+** icon and select **Expense Report**. +3. Tap **Add Expenses**, then select expenses. +4. Tap **Submit Report**, add approver details, and tap **Submit**. + +--- + +# 6. Add a Secondary Login + +Connect a personal email to ensure access to Expensify, even if your employer changes. + +*Setting up this feature is available only on the Expensify website.* + +1. Go to **Settings** > **Account**. +2. Under **Account Details**, click **Add Secondary Login**. +3. Enter your email or phone number. +4. Verify your new login with the Magic Code sent to you. + +--- + +# 7. Secure Your Account + +Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication (2FA). Setting this up requires you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) to log in. + +*Setting up this feature is available only on the Expensify website.* + +1. Go to **Settings** > **Account**. +2. Under **Two-Factor Authentication**, enable the toggle. +3. Save a copy of your backup codes. +4. Use an authenticator app to scan the QR code. +5. Enter the 6-digit code from the app and click **Verify**. + +--- + diff --git a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md b/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md deleted file mode 100644 index 6067df6874d4..000000000000 --- a/docs/articles/expensify-classic/getting-started/Join-your-company's-workspace.md +++ /dev/null @@ -1,259 +0,0 @@ ---- -title: Join your company's workspace -description: Get started with Expensify as an employee or other company member ---- -
- -# Overview - -Welcome to Expensify! If you received an invitation to join your company’s Expensify workspace, follow the steps below to get started. - -# 1. Download the mobile app - -Upload your expenses and check your reports right from your phone by downloading the Expensify mobile app. You can search for “Expensify” in the app store, or tap one of the links below. - -[iOS](https://apps.apple.com/us/app/expensify-expense-tracker/id471713959) -| [Android](https://play.google.com/store/apps/details?id=org.me.mobiexpensifyg&hl=en_US&gl=US) - -For a full walkthrough on creating and submitting expenses via the mobile app, click [here](https://expensify.navattic.com/fl150n1n)! - -# 2. Add your name and photo - -{% include selector.html values="desktop, mobile" %} -{% include option.html value="desktop" %} -
    -
  1. Click the profile image at the top of the main menu.
  2. -
  3. Hover over the profile picture and click Change.
  4. -
  5. Update your profile picture and name. -
      -
    • Name: Enter your first and last name into the fields and click Update. Note that this name will be visible to anyone in your company workspace.
    • -
    • Photo: Click Add Photo.
    • -
    -
  6. -
- -{% include end-option.html %} - -{% include option.html value="mobile" %} - -
    -
  1. Tap the ☰ menu icon in the top left.
  2. -
  3. Tap the profile picture icon.
  4. -
  5. Tap the Edit icon next to your name and update your name or photo. -
      -
    • Name: Enter your first and/or last name into the fields and tap Update. Note that this name will be visible to anyone in your company workspace.
    • -
    • Photo: Tap Upload Photo and either:
    • -
        -
      • Tap the capture button to take a new photo.
      • -
      • Tap the photo icon on the left to select a saved photo.
      • -
      -
    -
  6. -
- -{% include end-option.html %} -{% include end-selector.html %} - - -# 3. Meet Concierge -Your personal assistant, Concierge, lives on your Expensify Home page on both desktop and the mobile app. - -Concierge will walk you through setting up your account and also provide: - - -You can also get support at any time by clicking the green chat bubble in the right corner. This will open a chat with Concierge where you can ask questions and receive direct support. - -# 4. Learn how to add an expense -As an employee, you may need to document reimbursable expenses (like business travel paid for with personal funds) or non-reimbursable expenses (like a lunch paid for with a company card). You can create an expense automatically by SmartScanning a receipt, or you can enter it manually. - -## SmartScan a receipt - -You can upload pictures of your receipts to Expensify, and SmartScan will automatically capture the receipt details, including the merchant, date, total, and currency. - -{% include selector.html values="desktop, mobile" %} -{% include option.html value="desktop" %} -
    -
  1. Click the Expenses tab.
  2. -
  3. Click the + icon in the top right and select Scan Receipt.
  4. -
  5. Upload a saved image of a receipt.
  6. -
- -{% include end-option.html %} - -{% include option.html value="mobile" %} -
    -
  1. Open the mobile app and tap the camera icon in the bottom right corner.
  2. -
  3. Upload or take a photo of your receipt.
  4. - -
  5. Normal Mode: Upload one receipt.
  6. -
  7. Rapid Fire Mode: Upload multiple receipts at once.
  8. -
-{% include end-option.html %} -{% include end-selector.html %} - -You can open any receipt and select **Fill out details myself** to add or edit the merchant, date, total, description, category, or add attendees who took part in the expense. You can also check that the expense is correctly labeled as reimbursable or non-reimbursable and split the expense if multiple expenses are included on one receipt. - -*Note: You can also email receipts to SmartScan by sending them to receipts@expensify.com from an email address tied to your Expensify account (either a primary or secondary email). SmartScan will automatically pull all of the details from the receipt, fill them in for you, and add the receipt to the Expenses tab on your account.* - -## Manually enter an expense - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -
    -
  1. Click the Expenses tab.
  2. -
  3. Click the + icon in the top right.
  4. -
  5. Select the type of expense and enter the expense details.
  6. - -
  7. Click Save.
  8. -
-{% include end-option.html %} - -{% include option.html value="mobile" %} -
    -
  1. Tap the ☰ menu icon in the top left.
  2. -
  3. Tap Expenses.
  4. -
  5. Tap the + icon in the top right.
  6. -
  7. Tap the correct expense type and enter the expense details.
  8. - -
  9. Tap Save.
  10. -
-{% include end-option.html %} - -{% include end-selector.html %} - -# 5. Learn how to create & submit an expense report - -Once you’ve created your expenses, they may be automatically added to an expense report if your company has this feature enabled. If not, your next step will be to add your expenses to a report and submit them for payment. - -{% include selector.html values="Desktop, Mobile" %} - -{% include option.html value="desktop" %} - -
    -
  1. Click the Reports tab.
  2. - -
  3. Click New Report, or click the New Report dropdown and select Expense Report.
  4. -
  5. Click Add Expenses.
  6. -
  7. Click an expense to add it to the report.
  8. - -
  9. Once all your expenses are added to the report, click the X to close the pop-up.
  10. -
  11. (Optional) Make any desired changes to the report and/or expenses.
  12. - -
  13. When the report is ready to send for approval, click Submit.
  14. -
  15. Enter the details for who will receive a notification email about your report and what they will receive.
  16. - -
  17. Click Send.
  18. -
- -{% include end-option.html %} - -{% include option.html value="mobile" %} -
    -
  1. Tap the ☰ menu icon in the top left.
  2. -
  3. Tap Reports.
  4. - -
  5. Tap the + icon and tap Expense Report.
  6. -
  7. Tap Add Expenses, then tap an expense to add it to the report. Repeat this step until all desired expenses are added. Note: Only expenses that are not already on a report will appear.
  8. -
  9. (Optional) Make any desired changes to the report and/or expenses.
  10. - -
  11. When the report is ready to send for approval, tap Submit Report.
  12. -
  13. Add any additional sending details and tap Submit.
  14. -
  15. Enter the details for who will receive a notification email about your report and what they will receive.
  16. - -
  17. Tap Submit.
  18. -
-{% include end-option.html %} - -{% include end-selector.html %} - -# 6. Add a secondary login - -Connect your personal email address as a secondary login so you always have access to your Expensify account, even if your employer changes. - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -
    -
  1. Hover over Settings, then click Account.
  2. -
  3. Under the Account Details tab, scroll down to the Secondary Logins section and click Add Secondary Login.
  4. -
  5. Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable.
  6. -
  7. Find the email or text message from Expensify containing the Magic Code and enter it into the field to add the secondary login.
  8. -
- -# 7. Secure your account - -Add an extra layer of security to help keep your financial data safe and secure by enabling two-factor authentication. This will require you to enter a code generated by your preferred authenticator app (like Google Authenticator or Microsoft Authenticator) when you log in. - -*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* - -
    -
  1. Hover over Settings, then click Account.
  2. -
  3. Under the Account Details tab, scroll down to the Two Factor Authentication section and enable the toggle.
  4. -
  5. Save a copy of your backup codes. This step is critical—You will lose access to your account if you cannot use your authenticator app and do not have your recovery codes.
  6. - -
  7. Click Continue.
  8. -
  9. Download or open your authenticator app and either:
  10. - -
- -When you log in to Expensify in the future, you’ll open your authenticator app to get the code and enter it into Expensify. A new code regenerates every few seconds, so the code is always different. If the code time runs out, you can generate a new code as needed. - -
diff --git a/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md b/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md index 26db42df9e5b..5f5a54818330 100644 --- a/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md +++ b/docs/articles/new-expensify/connections/netsuite/Configure-Netsuite.md @@ -4,20 +4,20 @@ description: Configure the Import, Export, and Advanced settings for Expensify's order: 2 --- -# Best Practices Using NetSuite +# Best practices using NetSuite Using Expensify with NetSuite brings a seamless, efficient approach to managing expenses. With automatic syncing, expense reports flow directly into NetSuite, reducing manual entry and errors while giving real-time visibility into spending. This integration speeds up approvals, simplifies reimbursements, and provides clear insights for smarter budgeting and compliance. Together, Expensify and NetSuite make expense management faster, more accurate, and stress-free. -# Accessing the NetSuite Configuration Settings +# Accessing the NetSuite configuration settings NetSuite is connected at the workspace level, and each workspace can have a unique configuration that dictates how the connection functions. To access the connection settings: -1. Click your profile image or icon in the bottom left menu. +1. Click Settings in the bottom left menu. 2. Scroll down and click **Workspaces** in the left menu. 3. Select the workspace you want to access settings for. 4. Click **Accounting** in the left menu. -# Step 1: Configure Import Settings +# Step 1: Configure import settings The following steps help you determine how data will be imported from NetSuite to Expensify. @@ -25,10 +25,10 @@ The following steps help you determine how data will be imported from NetSuite t 2. In the right-hand menu, review each of the following import settings: - _Categories_: Your NetSuite Expense Categories are automatically imported into Expensify as categories. This is enabled by default and cannot be disabled. - _Department, Classes, and Locations_: The NetSuite connection allows you to import each independently and utilize tags, report fields, or employee defaults as the coding method. - - Tags are applied at the expense level and apply to single expense. + - Tags are applied at the expense level and apply to a single expense. - Report Fields are applied at the report header level and apply to all expenses on the report. - The employee default is applied when the expense is exported to NetSuite and comes from the default on the submitter’s employee record in NetSuite. - - _Customers and Projects_: The NetSuite connections allows you to import customers and projects into Expensify as Tags or Report Fields. + - _Customers and Projects_: The NetSuite connection allows you to import customers and projects into Expensify as Tags or Report Fields. -_Cross-subsidiary customers/projects_: Enable to import Customers and Projects across all NetSuite subsidiaries to a single Expensify workspace. This setting requires you to enable “Intercompany Time and Expense” in NetSuite. To enable that feature in NetSuite, go to **Setup > Company > Setup Tasks: Enable Features > Advanced Features**. -_Tax_: Enable to import NetSuite Tax Groups and configure further on the Taxes tab of your workspace settings menu. -_Custom Segments and Records_: Enable to import segments and records are tags or report fields. @@ -36,13 +36,13 @@ The following steps help you determine how data will be imported from NetSuite t - If configuring Custom Records as Tags, use the Field ID on the Transaction Columns tab (under **Custom Segments > Transaction Columns**). - Don’t use the “Filtered by” feature available for Custom Segments. Expensify can’t make these dependent on other fields. If you do have a filter selected, we suggest switching that filter in NetSuite to “Subsidiary” and enabling all subsidiaries to ensure you don’t receive any errors upon exporting reports. -_Custom Lists_: Enable to import lists as tags or reports fields. -3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option. Once the sync completes, you should see the values for any enabled tags or report fields in the corresponding Tag or Report Field tabs in the workspace settings menu. +3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option. Once the sync completes, you should see the values for any enabled tags or report fields in the corresponding Tag or Report Field tabs in the workspace settings menu. {% include info.html %} When you’re done configuring the settings, or anytime you make changes in the future, sync the NetSuite connection. This will ensure changes are saved and updated across both systems. {% include end-info.html %} -# Step 2: Configure Export Settings +# Step 2: Configure export settings The following steps help you determine how data will be exported from Expensify to NetSuite. @@ -59,7 +59,7 @@ The following steps help you determine how data will be exported from Expensify - _Journal Entries_: Out-of-pocket expenses will be exported to NetSuite as journal entries. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries. - By default, journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option. Also, the credit line and header-level classifications are pulled from the employee record. - _Export company card expenses as_: - - _Expense Reports_:To export company card expenses as expense reports, you will need to configure your default corporate cards in NetSuite. + - _Expense Reports_: To export company card expenses as expense reports, you must configure your default corporate cards in NetSuite. - _Vendor Bills_: Company card expenses will be posted as a vendor bill payable to the default vendor specified in your workspace Accounting settings. You can also set an approval level in NetSuite for the bills. - _Journal Entries_: Company Card expenses will be posted to the Journal Entries posting account selected in your workspace Accounting settings. - Important Notes: @@ -70,9 +70,9 @@ The following steps help you determine how data will be exported from Expensify - _Invoice item_: Choose whether Expensify creates an "Expensify invoice line item" for you upon export (if one doesn’t exist already) or select an existing invoice item. - _Export foreign currency amount_: Enabling this feature allows you to send the original amount of the expense rather than the converted total when exporting to NetSuite. This option is only available when exporting out-of-pocket expenses as Expense Reports. - _Export to next open period_: When this feature is enabled and you try exporting an expense report to a closed NetSuite period, we will automatically export to the next open period instead of returning an error. -3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option. +3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option. -# Step 3: Configure Advanced Settings +# Step 3: Configure advanced settings The following steps help you determine the advanced settings for your NetSuite connection. @@ -84,8 +84,8 @@ The following steps help you determine the advanced settings for your NetSuite c - _Sync reimbursed reports_: Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the NetSuite. - _Reimbursments account_: Select the account that matches the default account for Bill Payments in your NetSuite account. - _Collections account_: When exporting invoices, once marked as Paid, the payment is marked against the account selected. - - _Invite employees and set approvals_: Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace. - - In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.) + - _Invite employees and set approvals_: Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will email them to let them know they’ve been added to a workspace. + This feature invites employees and enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.) - _Auto create employees/vendors_: With this feature enabled, Expensify will automatically create a new employee or vendor in NetSuite (if one doesn’t already exist) using the name and email of the report submitter. - _Enable newly imported categories_: Toggle to enable this feature and anytime a new Expense Category is created in NetSuite, it will be imported into Expensify as an enabled category. Otherwise, it will import disabled and employees will be unable to see it as an option to code to an expense. - _Setting approval levels_: You can set the NetSuite approval level for each different export type; Expense report, Vendor bill, and Journal entry. @@ -93,7 +93,7 @@ The following steps help you determine the advanced settings for your NetSuite c - _Custom form ID_: By default, Expensify creates entries using the preferred transaction form set in NetSuite. Enabling this setting allows you to designate a specific transaction form. - _Out-of-pocket expense_: - _Company card expense_: -3. Sync the connection by closing the right-hand menu and clicking the three-dot icon > Sync Now option. +3. Sync the connection by closing the right-hand menu and clicking the **three-dot icon** > **Sync Now** option. {% include faq-begin.md %} @@ -107,20 +107,21 @@ Once imported, you can turn specific tags on or off under **Settings > Workspace Yes, you can automatically import your employees and set their approval workflow with your connection between NetSuite and Expensify. -Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will send them an email letting them know they’ve been added to a workspace. +Enabling this feature will invite all employees from the connected NetSuite subsidiary to your Expensify workspace. Once imported, Expensify will email them to let them know they’ve been added to a workspace. In addition to inviting employees, this feature enables a custom set of approval workflow options, which you can manage in Expensify Classic. (Click Switch to Expensify Classic from the Settings menu.) Your options for approval include: -- **Basic Approval:** A single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. +- **Basic Approval:** This is a single level of approval, where all users submit directly to a Final Approver. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Manager Approval (default):** Two levels of approval route reports first to an employee’s NetSuite expense approver or supervisor, and second to a workspace-wide Final Approver. By NetSuite convention, Expensify will map to the supervisor if no expense approver exists. The Final Approver defaults to the workspace owner but can be edited on the people page. - **Configure Manually:** Employees will be imported, but all levels of approval must be manually configured on the workspace’s People settings page. If you enable this setting, it’s recommended you review the newly imported employees and managers on the **Settings > Workspaces > Group > [Workspace Name] > People** page. ## I notice that company card expenses export to NetSuite right away when I approve a report, but reimbursable expenses don’t, why is that? -When Auto Sync is enabled and you reimburse employees through Expensify, we help to automatically send finalized expenses to NetSuite. The timing of the export depends on the type of expense it is. - - **If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite. - - **If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at the time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs. +When Auto Sync is enabled and you reimburse employees through Expensify, we help you automatically send finalized expenses to NetSuite. The timing of the export depends on the type of expense. + +- **If you reimburse members through Expensify:** Reimbursing an expense report will trigger auto-export to NetSuite. When the expense report is exported to NetSuite, a corresponding bill payment will also be created in NetSuite. +- **If you reimburse members outside of Expensify:** Expense reports will be exported to NetSuite at the time of final approval. After you mark the report as paid in NetSuite, the reimbursed status will be synced back to Expensify the next time the integration syncs. ## How do I configure my default corporate cards in NetSuite? @@ -147,7 +148,7 @@ You can also select the default account on your employee record to use individua If a report is reimbursed via ACH or marked as reimbursed in Expensify and then exported to NetSuite, the report is automatically marked as paid in NetSuite. -If a report is exported to NetSuite, and then marked as paid in NetSuite, the report will automatically be marked as reimbursed in Expensify during the next sync. +If a report is exported to NetSuite and then marked as paid in NetSuite, it will automatically be marked as reimbursed in Expensify during the next sync. ## Will enabling auto-sync affect existing approved and reimbursed reports? @@ -157,6 +158,6 @@ Auto-sync will only export newly approved reports to NetSuite. Reports that were When using multi-currency features with NetSuite, remember these points: -**Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors. -**Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error. + - **Employee/Vendor currency:** The currency set for a NetSuite vendor or employee record must match the subsidiary currency for whichever subsidiary you export that user's reports to. A currency mismatch will cause export errors. + - **Bank Account Currency:** When synchronizing bill payments, your bank account’s currency must match the subsidiary’s currency. Failure to do so will result in an “Invalid Account” error. {% include faq-end.md %} diff --git a/docs/redirects.csv b/docs/redirects.csv index 7775a05a1411..fb692d257c90 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -126,7 +126,6 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-c https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Reconciliation,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Reconciliation https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Troubleshooting,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Troubleshooting -https://help.expensify.com/articles/expensify-classic/reports/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules https://help.expensify.com/articles/expensify-classic/reports/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency https://help.expensify.com/articles/expensify-classic/reports/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page https://help.expensify.com/articles/expensify-classic/reports/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses @@ -145,7 +144,6 @@ https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/App https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Invite-Members,https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Attendee-Tracking,https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Currency,https://help.expensify.com/articles/expensify-classic/workspaces/Currency -https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Expense-Types,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Types https://help.expensify.com/articles/expensify-classic/expense-and-report-features/Report-Audit-Log-and-Comments,https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-and-Comments https://help.expensify.com/articles/expensify-classic/expense-and-report-features/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page @@ -592,7 +590,6 @@ https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-Overview -https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules https://help.expensify.com/articles/expensify-classic/expenses/The-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/Navigate-the-Expenses-Page https://help.expensify.com/articles/expensify-classic/expenses/Add-expenses-in-bulk,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense https://help.expensify.com/articles/expensify-classic/expenses/Track-group-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Add-an-expense @@ -651,8 +648,11 @@ https://help.expensify.com/articles/expensify-classic/workspaces/Create-a-group- https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-company-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-Company-Workspace https://help.expensify.com/articles/expensify-classic/workspaces/Set-up-your-individual-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Create-a-workspace-for-yourself https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel,https://help.expensify.com/articles/new-expensify/travel/Book-with-Expensify-Travel +https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace,https://help.expensify.com/articles/expensify-classic/getting-started/Join-Your-Company's-Workspace https://help.expensify.com/articles/expensify-classic/domains/Add-Domain-Members-and-Admins,https://help.expensify.com/articles/expensify-classic/domains/Domain-Members https://help.expensify.com/articles/expensify-classic/domains/Switch-Domain-Member-to-Admin,https://help.expensify.com/articles/expensify-classic/domains/Domain-Members +https://help.expensify.com/articles/expensify-classic/domains/Create-A-Group,https://help.expensify.com/articles/expensify-classic/domains/Domain-Groups https://help.expensify.com/articles/expensify-classic/expenses/Navigate-the-Expenses-Page,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page https://help.expensify.com/articles/expensify-classic/expenses/Export-expenses,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page https://help.expensify.com/articles/expensify-classic/reports/Print-or-download-a-report,https://help.expensify.com/articles/expensify-classic/expenses/Export-Expenses-from-the-Expenses-Page +https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 754aad26679e..4f85a21d3a14 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.1.6 + 9.1.7 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.1.6.0 + 9.1.7.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3b820bfc9d5a..d81a54efbefd 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.1.6 + 9.1.7 CFBundleSignature ???? CFBundleVersion - 9.1.6.0 + 9.1.7.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8db6346d0067..2df8e2a311bd 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.1.6 + 9.1.7 CFBundleVersion - 9.1.6.0 + 9.1.7.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index d3e395b5b442..51273e15925b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.1.6-0", + "version": "9.1.7-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.1.6-0", + "version": "9.1.7-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 94acee81e628..f5188ea8fc63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.1.6-0", + "version": "9.1.7-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index ee924eb883ff..75db0a61a28e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -90,7 +90,7 @@ const onboardingChoices = { ...backendOnboardingChoices, } as const; -const combinedTrackSubmitOnboardingChoices = { +const createExpenseOnboardingChoices = { PERSONAL_SPEND: selectableOnboardingChoices.PERSONAL_SPEND, EMPLOYER: selectableOnboardingChoices.EMPLOYER, SUBMIT: backendOnboardingChoices.SUBMIT, @@ -124,16 +124,6 @@ const createWorkspaceTask: OnboardingTask = { `*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`, }; -const meetGuideTask: OnboardingTask = { - type: 'meetGuide', - autoCompleted: false, - title: 'Meet your setup specialist', - description: ({adminsRoomLink}) => - `Meet your setup specialist, who can answer any questions as you get started with Expensify. Yes, a real human!\n` + - '\n' + - `Chat with the specialist in your [#admins room](${adminsRoomLink}).`, -}; - const setupCategoriesTask: OnboardingTask = { type: 'setupCategories', autoCompleted: false, @@ -756,7 +746,6 @@ const CONST = { PREVENT_SPOTNANA_TRAVEL: 'preventSpotnanaTravel', REPORT_FIELDS_FEATURE: 'reportFieldsFeature', NETSUITE_USA_TAX: 'netsuiteUsaTax', - COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', PER_DIEM: 'newDotPerDiem', NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts', NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest', @@ -2961,6 +2950,7 @@ const CONST = { BREX: 'oauth.brex.com', WELLS_FARGO: 'oauth.wellsfargo.com', AMEX_DIRECT: 'oauth.americanexpressfdx.com', + CSV: '_ccupload', }, STEP_NAMES: ['1', '2', '3', '4'], STEP: { @@ -3041,6 +3031,7 @@ const CONST = { VISA: 'visa', MASTERCARD: 'mastercard', STRIPE: 'stripe', + CSV: 'CSV', }, FEED_TYPE: { CUSTOM: 'customFeed', @@ -3330,7 +3321,7 @@ const CONST = { GUIDES_CALL_TASK_IDS: { CONCIERGE_DM: 'NewExpensifyConciergeDM', WORKSPACE_INITIAL: 'WorkspaceHome', - WORKSPACE_PROFILE: 'WorkspaceProfile', + WORKSPACE_OVERVIEW: 'WorkspaceOverview', WORKSPACE_INVOICES: 'WorkspaceSendInvoices', WORKSPACE_MEMBERS: 'WorkspaceManageMembers', WORKSPACE_EXPENSIFY_CARD: 'WorkspaceExpensifyCard', @@ -5163,7 +5154,7 @@ const CONST = { ONBOARDING_CHOICES: {...onboardingChoices}, SELECTABLE_ONBOARDING_CHOICES: {...selectableOnboardingChoices}, - COMBINED_TRACK_SUBMIT_ONBOARDING_CHOICES: {...combinedTrackSubmitOnboardingChoices}, + CREATE_EXPENSE_ONBOARDING_CHOICES: {...createExpenseOnboardingChoices}, ONBOARDING_SIGNUP_QUALIFIERS: {...signupQualifiers}, ONBOARDING_INVITE_TYPES: {...onboardingInviteTypes}, ONBOARDING_COMPANY_SIZE: {...onboardingCompanySize}, @@ -5184,7 +5175,6 @@ const CONST = { tasks: [ createWorkspaceTask, selfGuidedTourTask, - meetGuideTask, { type: 'setupCategoriesAndTags', autoCompleted: false, @@ -5283,7 +5273,6 @@ const CONST = { }, tasks: [ createWorkspaceTask, - meetGuideTask, setupCategoriesTask, { type: 'inviteAccountant', @@ -5352,7 +5341,6 @@ const CONST = { [onboardingChoices.ADMIN]: { message: "As an admin, learn how to manage your team's workspace and submit expenses yourself.", tasks: [ - meetGuideTask, { type: 'reviewWorkspaceSettings', autoCompleted: false, @@ -5390,11 +5378,11 @@ const CONST = { }, } satisfies Record, - COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES: { - [combinedTrackSubmitOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage, - [combinedTrackSubmitOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, - [combinedTrackSubmitOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, - } satisfies Record, OnboardingMessage>, + CREATE_EXPENSE_ONBOARDING_MESSAGES: { + [createExpenseOnboardingChoices.PERSONAL_SPEND]: combinedTrackSubmitOnboardingPersonalSpendMessage, + [createExpenseOnboardingChoices.EMPLOYER]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + [createExpenseOnboardingChoices.SUBMIT]: combinedTrackSubmitOnboardingEmployerOrSubmitMessage, + } satisfies Record, OnboardingMessage>, REPORT_FIELD_TITLE_FIELD_ID: 'text_title', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 50a768955b25..86504d344c96 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -809,35 +809,35 @@ const ROUTES = { route: 'settings/workspaces/:policyID/invite-message', getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`settings/workspaces/${policyID}/invite-message`, backTo)}` as const, }, - WORKSPACE_PROFILE: { - route: 'settings/workspaces/:policyID/profile', + WORKSPACE_OVERVIEW: { + route: 'settings/workspaces/:policyID/overview', getRoute: (policyID: string | undefined, backTo?: string) => { if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE route'); + Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW route'); } - return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile` as const, backTo); + return getUrlWithBackToParam(`settings/workspaces/${policyID}/overview` as const, backTo); }, }, - WORKSPACE_PROFILE_ADDRESS: { - route: 'settings/workspaces/:policyID/profile/address', + WORKSPACE_OVERVIEW_ADDRESS: { + route: 'settings/workspaces/:policyID/overview/address', getRoute: (policyID: string | undefined, backTo?: string) => { if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE_ADDRESS route'); + Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW_ADDRESS route'); } - return getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo); + return getUrlWithBackToParam(`settings/workspaces/${policyID}/overview/address` as const, backTo); }, }, - WORKSPACE_PROFILE_PLAN: { - route: 'settings/workspaces/:policyID/profile/plan', - getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + WORKSPACE_OVERVIEW_PLAN: { + route: 'settings/workspaces/:policyID/overview/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/overview/plan` as const, backTo), }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, }, - WORKSPACE_PROFILE_CURRENCY: { - route: 'settings/workspaces/:policyID/profile/currency', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/currency` as const, + WORKSPACE_OVERVIEW_CURRENCY: { + route: 'settings/workspaces/:policyID/overview/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/currency` as const, }, POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: { route: 'settings/workspaces/:policyID/accounting/quickbooks-online/export', @@ -987,22 +987,22 @@ const ROUTES = { route: 'settings/workspaces/:policyID/accounting/quickbooks-desktop/import/items', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-desktop/import/items` as const, }, - WORKSPACE_PROFILE_NAME: { - route: 'settings/workspaces/:policyID/profile/name', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/name` as const, + WORKSPACE_OVERVIEW_NAME: { + route: 'settings/workspaces/:policyID/overview/name', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/name` as const, }, - WORKSPACE_PROFILE_DESCRIPTION: { - route: 'settings/workspaces/:policyID/profile/description', + WORKSPACE_OVERVIEW_DESCRIPTION: { + route: 'settings/workspaces/:policyID/overview/description', getRoute: (policyID: string | undefined) => { if (!policyID) { - Log.warn('Invalid policyID is used to build the WORKSPACE_PROFILE_DESCRIPTION route'); + Log.warn('Invalid policyID is used to build the WORKSPACE_OVERVIEW_DESCRIPTION route'); } - return `settings/workspaces/${policyID}/profile/description` as const; + return `settings/workspaces/${policyID}/overview/description` as const; }, }, - WORKSPACE_PROFILE_SHARE: { - route: 'settings/workspaces/:policyID/profile/share', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/share` as const, + WORKSPACE_OVERVIEW_SHARE: { + route: 'settings/workspaces/:policyID/overview/share', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/overview/share` as const, }, WORKSPACE_AVATAR: { route: 'settings/workspaces/:policyID/avatar', @@ -1473,7 +1473,7 @@ const ROUTES = { }, WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT: { route: 'settings/workspaces/:policyID/expensify-card/settings/account', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/account` as const, + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/settings/account`, backTo), }, WORKSPACE_EXPENSIFY_CARD_SETTINGS_FREQUENCY: { route: 'settings/workspaces/:policyID/expensify-card/settings/frequency', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index d950fb1cd5db..5a8b0c75d5c0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -468,7 +468,7 @@ const SCREENS = { MULTI_CONNECTION_SELECTOR: 'Policy_Accounting_Multi_Connection_Selector', }, INITIAL: 'Workspace_Initial', - PROFILE: 'Workspace_Profile', + PROFILE: 'Workspace_Overview', COMPANY_CARDS: 'Workspace_CompanyCards', COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard', COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', @@ -531,9 +531,9 @@ const SCREENS = { TAG_APPROVER: 'Tag_Approver', TAG_LIST_VIEW: 'Tag_List_View', TAG_GL_CODE: 'Tag_GL_Code', - CURRENCY: 'Workspace_Profile_Currency', - ADDRESS: 'Workspace_Profile_Address', - PLAN: 'Workspace_Profile_Plan_Type', + CURRENCY: 'Workspace_Overview_Currency', + ADDRESS: 'Workspace_Overview_Address', + PLAN: 'Workspace_Overview_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', @@ -542,9 +542,9 @@ const SCREENS = { WORKFLOWS_APPROVALS_APPROVER: 'Workspace_Workflows_Approvals_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', - DESCRIPTION: 'Workspace_Profile_Description', - SHARE: 'Workspace_Profile_Share', - NAME: 'Workspace_Profile_Name', + DESCRIPTION: 'Workspace_Overview_Description', + SHARE: 'Workspace_Overview_Share', + NAME: 'Workspace_Overview_Name', CATEGORY_CREATE: 'Category_Create', CATEGORY_EDIT: 'Category_Edit', CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code', diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 846cf23e4c8a..2c442df75060 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -73,7 +73,7 @@ function BookTravelButton({text}: BookTravelButtonProps) { // Spotnana requires an address anytime an entity is created for a policy if (isEmptyObject(policy?.address)) { - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute())); return; } diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 0bc3dba642cf..bffac55007fa 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -3,8 +3,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; -import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; -import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; +import {isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; +import {isCurrentUserSubmitter, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -52,7 +52,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn ); } - if (isReportApproved({report}) || isReportManuallyReimbursed(report) || (isProcessingReport(report) && !isInstantSubmitEnabled(policy))) { + if (isReportApproved({report}) || isReportManuallyReimbursed(report)) { return translate('violations.memberBrokenConnectionError'); } diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 69e542e44798..058b2e564f89 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -89,7 +89,7 @@ function EmptyStateComponent({ contentContainerStyle={[{minHeight: minModalHeight}, styles.flexGrow1, styles.flexShrink0, containerStyles]} style={styles.flex1} > - + [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], - [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale], + () => [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale, transactions], + [reportActions, reports, reportNameValuePairs, transactionViolations, policy, personalDetails, data.length, draftComments, optionMode, preferredLocale, transactions], ); const previousOptionMode = usePrevious(optionMode); diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 9c47dd0da547..9cd82fd4fad5 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -10,7 +10,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getConnectedIntegration} from '@libs/PolicyUtils'; import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import { canBeExported, @@ -27,13 +27,13 @@ import { isAllowedToSubmitDraftExpenseReport, isArchivedReportWithID, isClosedExpenseReportWithNoExpenses, - isCurrentUserSubmitter, isInvoiceReport, navigateBackOnDeleteTransaction, reportTransactionsSelector, } from '@libs/ReportUtils'; import { allHavePendingRTERViolation, + checkIfShouldShowMarkAsCashButton, isDuplicate as isDuplicateTransactionUtils, isExpensifyCardTransaction, isOnHold as isOnHoldTransactionUtils, @@ -150,11 +150,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const hasOnlyPendingTransactions = useMemo(() => { return !!transactions && transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); }, [transactions]); - const transactionIDs = transactions?.map((t) => t.transactionID) ?? []; - const [violations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, { - selector: (allTransactions) => - Object.fromEntries(Object.entries(allTransactions ?? {}).filter(([key]) => transactionIDs.includes(key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')))), - }); + const transactionIDs = useMemo(() => transactions?.map((t) => t.transactionID) ?? [], [transactions]); + const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const violations = useMemo( + () => Object.fromEntries(Object.entries(allViolations ?? {}).filter(([key]) => transactionIDs.includes(key.replace(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, '')))), + [allViolations, transactionIDs], + ); // Check if there is pending rter violation in all transactionViolations with given transactionIDs. const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDs, violations); // Check if user should see broken connection violation warning. @@ -173,8 +174,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowMarkAsCashButton = - !!transactionThreadReportID && - (hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(moneyRequestReport?.reportID)))); + !!transactionThreadReportID && checkIfShouldShowMarkAsCashButton(hasAllPendingRTERViolations, shouldShowBrokenConnectionViolation, moneyRequestReport, policy); const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere; @@ -300,7 +300,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (hasOnlyHeldExpenses) { return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')}; } - if (shouldShowBrokenConnectionViolation) { + if (!!transaction?.transactionID && shouldShowBrokenConnectionViolation) { return { icon: getStatusIcon(Expensicons.Hourglass), description: ( diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5dfbbe9fa727..aed7163b2921 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -10,10 +10,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import Navigation from '@libs/Navigation/Navigation'; -import {isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {isCurrentUserSubmitter} from '@libs/ReportUtils'; import { + checkIfShouldShowMarkAsCashButton, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, hasReceipt, isDuplicate as isDuplicateTransactionUtils, @@ -83,8 +82,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(transactionViolations); - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction, report, policy, transactionViolations); - const shouldShowMarkAsCashButton = hasPendingRTERViolation || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction, parentReport, policy, transactionViolations); + const shouldShowMarkAsCashButton = checkIfShouldShowMarkAsCashButton(hasPendingRTERViolation, shouldShowBrokenConnectionViolation, parentReport, policy); const markAsCash = useCallback(() => { markAsCashAction(transaction?.transactionID, reportID); @@ -115,7 +114,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre description: ( ), diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index 020553d9b1f5..0fb38f36ebd9 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -258,7 +258,7 @@ function SearchPageHeaderInput({ {showPopupButton && ( + + + + + ); } diff --git a/src/components/ValuePicker/index.tsx b/src/components/ValuePicker/index.tsx index cb604c855cb4..80974aeb78a6 100644 --- a/src/components/ValuePicker/index.tsx +++ b/src/components/ValuePicker/index.tsx @@ -2,15 +2,12 @@ import React, {forwardRef, useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import useStyleUtils from '@hooks/useStyleUtils'; import Navigation from '@libs/Navigation/Navigation'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {ValuePickerItem, ValuePickerProps} from './types'; import ValueSelectorModal from './ValueSelectorModal'; function ValuePicker({value, label, items, placeholder = '', errorText = '', onInputChange, furtherDetails, shouldShowTooltips = true}: ValuePickerProps, forwardedRef: ForwardedRef) { - const StyleUtils = useStyleUtils(); const [isPickerVisible, setIsPickerVisible] = useState(false); const showPickerModal = () => { @@ -28,7 +25,6 @@ function ValuePicker({value, label, items, placeholder = '', errorText = '', onI hidePickerModal(); }; - const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; const selectedItem = items?.find((item) => item.value === value); return ( @@ -38,7 +34,6 @@ function ValuePicker({value, label, items, placeholder = '', errorText = '', onI shouldShowRightIcon // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing title={selectedItem?.label || placeholder || ''} - descriptionTextStyle={descStyle} description={label} onPress={showPickerModal} furtherDetails={furtherDetails} diff --git a/src/languages/en.ts b/src/languages/en.ts index 68e1649e1fa0..73da02793813 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5019,6 +5019,7 @@ const translations = { cardFeeds: 'Card feeds', cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) => `All ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`, + cardFeedNameCSV: ({cardFeedLabel}: {cardFeedLabel?: string}) => `All CSV Imported Cards${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`, }, current: 'Current', past: 'Past', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9ab166330e07..0d54c36a90cf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5068,6 +5068,7 @@ const translations = { cardFeeds: 'Flujos de tarjetas', cardFeedName: ({cardFeedBankName, cardFeedLabel}: {cardFeedBankName: string; cardFeedLabel?: string}) => `Todo ${cardFeedBankName}${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`, + cardFeedNameCSV: ({cardFeedLabel}: {cardFeedLabel?: string}) => `Todas las Tarjetas Importadas desde CSV${cardFeedLabel ? ` - ${cardFeedLabel}` : ''}`, }, amount: { lessThan: ({amount}: OptionalParam = {}) => `Menos de ${amount ?? ''}`, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c1a0341c7166..03b5103a6491 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1000,6 +1000,7 @@ const READ_COMMANDS = { GET_ASSIGNED_SUPPORT_DATA: 'GetAssignedSupportData', OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage', GET_CORPAY_ONBOARDING_FIELDS: 'GetCorpayOnboardingFields', + OPEN_SECURITY_SETTINGS_PAGE: 'OpenSecuritySettingsPage', } as const; type ReadCommand = ValueOf; @@ -1069,6 +1070,7 @@ type ReadCommandParameters = { [READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA]: Parameters.GetAssignedSupportDataParams; [READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams; [READ_COMMANDS.GET_CORPAY_ONBOARDING_FIELDS]: Parameters.GetCorpayOnboardingFieldsParams; + [READ_COMMANDS.OPEN_SECURITY_SETTINGS_PAGE]: null; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 088c8d2f6e5e..3024bd21e394 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; +import type IllustrationsType from '@styles/theme/illustrations/types'; import * as Illustrations from '@src/components/Icon/Illustrations'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -261,7 +262,7 @@ function sortCardsByCardholderName(cardsList: OnyxEntry, per }); } -function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK): IconAsset { +function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD.BANK, illustrations: IllustrationsType): IconAsset { const feedIcons = { [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: Illustrations.VisaCompanyCardDetailLarge, [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: Illustrations.AmexCardCompanyCardDetailLarge, @@ -274,6 +275,7 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD [CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: Illustrations.WellsFargoCompanyCardDetailLarge, [CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: Illustrations.BrexCompanyCardDetailLarge, [CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE]: Illustrations.StripeCompanyCardDetailLarge, + [CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]: illustrations.GenericCSVCompanyCardLarge, [CONST.EXPENSIFY_CARD.BANK]: ExpensifyCardImage, }; @@ -292,7 +294,11 @@ function getCardFeedIcon(cardFeed: CompanyCardFeed | typeof CONST.EXPENSIFY_CARD return feedIcons[feedKey]; } - return Illustrations.AmexCompanyCards; + if (cardFeed.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)) { + return illustrations.GenericCSVCompanyCardLarge; + } + + return illustrations.GenericCompanyCardLarge; } /** @@ -326,11 +332,16 @@ function getBankName(feedType: CompanyCardFeed): string { [CONST.COMPANY_CARD.FEED_BANK_NAME.CITIBANK]: 'Citibank', [CONST.COMPANY_CARD.FEED_BANK_NAME.WELLS_FARGO]: 'Wells Fargo', [CONST.COMPANY_CARD.FEED_BANK_NAME.BREX]: 'Brex', + [CONST.COMPANY_CARD.FEED_BANK_NAME.CSV]: CONST.COMPANY_CARDS.CARD_TYPE.CSV, }; // In existing OldDot setups other variations of feeds could exist, ex: vcf2, vcf3, oauth.americanexpressfdx.com 2003 const feedKey = (Object.keys(feedNamesMapping) as CompanyCardFeed[]).find((feed) => feedType.startsWith(feed)); + if (feedType.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)) { + return CONST.COMPANY_CARDS.CARD_TYPE.CSV; + } + if (!feedKey) { return ''; } @@ -338,7 +349,7 @@ function getBankName(feedType: CompanyCardFeed): string { return feedNamesMapping[feedKey]; } -const getBankCardDetailsImage = (bank: ValueOf): IconAsset => { +const getBankCardDetailsImage = (bank: ValueOf, illustrations: IllustrationsType): IconAsset => { const iconMap: Record, IconAsset> = { [CONST.COMPANY_CARDS.BANKS.AMEX]: Illustrations.AmexCardCompanyCardDetail, [CONST.COMPANY_CARDS.BANKS.BANK_OF_AMERICA]: Illustrations.BankOfAmericaCompanyCardDetail, @@ -348,7 +359,7 @@ const getBankCardDetailsImage = (bank: ValueOf [CONST.COMPANY_CARDS.BANKS.WELLS_FARGO]: Illustrations.WellsFargoCompanyCardDetail, [CONST.COMPANY_CARDS.BANKS.BREX]: Illustrations.BrexCompanyCardDetail, [CONST.COMPANY_CARDS.BANKS.STRIPE]: Illustrations.StripeCompanyCardDetail, - [CONST.COMPANY_CARDS.BANKS.OTHER]: Illustrations.OtherCompanyCardDetail, + [CONST.COMPANY_CARDS.BANKS.OTHER]: illustrations.GenericCompanyCard, }; return iconMap[bank]; }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 2058a317aa2b..91939c86f07f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -279,12 +279,12 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceInviteMessagePage').default, [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default, [SCREENS.WORKSPACE.NAME]: () => require('../../../../pages/workspace/WorkspaceNamePage').default, - [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../../pages/workspace/WorkspaceProfileDescriptionPage').default, - [SCREENS.WORKSPACE.SHARE]: () => require('../../../../pages/workspace/WorkspaceProfileSharePage').default, - [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../../pages/workspace/WorkspaceProfileCurrencyPage').default, + [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../../pages/workspace/WorkspaceOverviewDescriptionPage').default, + [SCREENS.WORKSPACE.SHARE]: () => require('../../../../pages/workspace/WorkspaceOverviewSharePage').default, + [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../../pages/workspace/WorkspaceOverviewCurrencyPage').default, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default, - [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceProfileAddressPage').default, - [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceProfilePlanTypePage').default, + [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceOverviewAddressPage').default, + [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceOverviewPlanTypePage').default, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORT]: () => require('../../../../pages/workspace/categories/ImportCategoriesPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: () => require('../../../../pages/workspace/categories/ImportedCategoriesPage').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx index c1b0fcc481f7..6ee46975618d 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx @@ -15,7 +15,7 @@ type Screens = Partial Reac const loadWorkspaceInitialPage = () => require('../../../../pages/workspace/WorkspaceInitialPage').default; const CENTRAL_PANE_WORKSPACE_SCREENS = { - [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default, + [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceOverviewPage').default, [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default, [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default, [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../pages/workspace/WorkspaceMembersPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b6c57aa1eaf5..90d14b50890a 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -303,13 +303,13 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION, }, [SCREENS.WORKSPACE.CURRENCY]: { - path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route, + path: ROUTES.WORKSPACE_OVERVIEW_CURRENCY.route, }, [SCREENS.WORKSPACE.ADDRESS]: { - path: ROUTES.WORKSPACE_PROFILE_ADDRESS.route, + path: ROUTES.WORKSPACE_OVERVIEW_ADDRESS.route, }, [SCREENS.WORKSPACE.PLAN]: { - path: ROUTES.WORKSPACE_PROFILE_PLAN.route, + path: ROUTES.WORKSPACE_OVERVIEW_PLAN.route, }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route}, @@ -559,7 +559,7 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route}, [SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR]: {path: ROUTES.WORKSPACE_ACCOUNTING_MULTI_CONNECTION_SELECTOR.route}, [SCREENS.WORKSPACE.DESCRIPTION]: { - path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route, + path: ROUTES.WORKSPACE_OVERVIEW_DESCRIPTION.route, }, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: { path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.route, @@ -568,7 +568,7 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_MONTHLY_OFFSET.route, }, [SCREENS.WORKSPACE.SHARE]: { - path: ROUTES.WORKSPACE_PROFILE_SHARE.route, + path: ROUTES.WORKSPACE_OVERVIEW_SHARE.route, }, [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: { path: ROUTES.WORKSPACE_INVOICES_COMPANY_NAME.route, @@ -878,7 +878,7 @@ const config: LinkingOptions['config'] = { [SCREENS.KEYBOARD_SHORTCUTS]: { path: ROUTES.KEYBOARD_SHORTCUTS, }, - [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, + [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_OVERVIEW_NAME.route, [SCREENS.SETTINGS.SHARE_CODE]: { path: ROUTES.SETTINGS_SHARE_CODE, }, @@ -1585,7 +1585,7 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.INITIAL]: { path: ROUTES.WORKSPACE_INITIAL.route, }, - [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_PROFILE.route, + [SCREENS.WORKSPACE.PROFILE]: ROUTES.WORKSPACE_OVERVIEW.route, [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 98ea3606e6c1..fc73a6db8626 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -854,6 +854,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: { policyID: string; + backTo?: Routes; }; [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: { policyID: string; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index e3d04034abeb..5745dd0b7dc8 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -26,7 +26,17 @@ import { isPolicyAdmin, } from '@libs/PolicyUtils'; import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {getReportTransactions, isOpenExpenseReport, isProcessingReport, isReportIDApproved, isSettled, isThread} from '@libs/ReportUtils'; +import { + getReportTransactions, + isCurrentUserSubmitter, + isOpenExpenseReport, + isProcessingReport, + isReportApproved, + isReportIDApproved, + isReportManuallyReimbursed, + isSettled, + isThread, +} from '@libs/ReportUtils'; import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; @@ -820,7 +830,7 @@ function shouldShowBrokenConnectionViolation( // This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time. throw new Error('Invalid argument combination. If a transactionIDList is passed in, then an OnyxCollection of violations is expected'); } - violations = transactionOrIDList.flatMap((id) => transactionViolations?.[id] ?? []); + violations = transactionOrIDList.flatMap((id) => transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? []); } else { if (!Array.isArray(transactionViolations)) { // This should not be possible except in the case of incorrect type assertions. Generally TS should prevent this at compile time. @@ -830,7 +840,24 @@ function shouldShowBrokenConnectionViolation( } const brokenConnectionViolations = violations.filter((violation) => isBrokenConnectionViolation(violation)); - return brokenConnectionViolations.length > 0 && (!isPolicyAdmin(policy) || isOpenExpenseReport(report) || (isProcessingReport(report) && isInstantSubmitEnabled(policy))); + + if (brokenConnectionViolations.length > 0) { + if (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) { + return true; + } + return isOpenExpenseReport(report) || (isProcessingReport(report) && isInstantSubmitEnabled(policy)); + } + + return false; +} + +function checkIfShouldShowMarkAsCashButton(hasRTERVPendingViolation: boolean, shouldDisplayBrokenConnectionViolation: boolean, report: OnyxEntry, policy: OnyxEntry) { + if (hasRTERVPendingViolation) { + return true; + } + return ( + shouldDisplayBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(report?.reportID)) && !isReportApproved({report}) && !isReportManuallyReimbursed(report) + ); } /** @@ -1518,6 +1545,7 @@ export { isPerDiemRequest, isViolationDismissed, isBrokenConnectionViolation, + checkIfShouldShowMarkAsCashButton, shouldShowRTERViolationMessage, }; diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index e955b69ec7ab..39ada8cf2a10 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -3,7 +3,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type {AddDelegateParams, RemoveDelegateParams, UpdateDelegateRoleParams} from '@libs/API/parameters'; -import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; import * as NetworkStore from '@libs/Network/NetworkStore'; @@ -673,6 +673,10 @@ function restoreDelegateSession(authenticateResponse: Response) { }); } +function openSecuritySettingsPage() { + API.read(READ_COMMANDS.OPEN_SECURITY_SETTINGS_PAGE, null); +} + export { connect, disconnect, @@ -687,5 +691,6 @@ export { clearDelegateRolePendingAction, updateDelegateRole, removeDelegate, + openSecuritySettingsPage, KEYS_TO_PRESERVE_DELEGATE_ACCESS, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4e373f614274..4ec5ac4c9dd8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -122,6 +122,7 @@ import { isInvoiceReport as isInvoiceReportReportUtils, isInvoiceRoom, isMoneyRequestReport as isMoneyRequestReportReportUtils, + isOneOnOneChat, isOpenExpenseReport as isOpenExpenseReportReportUtils, isOpenInvoiceReport as isOpenInvoiceReportReportUtils, isOptimisticPersonalDetail, @@ -344,7 +345,7 @@ type PerDiemExpenseTransactionParams = { billable?: boolean; }; -type RequestMoneyPolicyParams = { +type BasePolicyParams = { policy?: OnyxEntry; policyTagList?: OnyxEntry; policyCategories?: OnyxEntry; @@ -359,7 +360,7 @@ type RequestMoneyParticipantParams = { type PerDiemExpenseInformation = { report: OnyxEntry; participantParams: RequestMoneyParticipantParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; transactionParams: PerDiemExpenseTransactionParams; }; @@ -367,14 +368,14 @@ type PerDiemExpenseInformationParams = { parentChatReport: OnyxEntry; transactionParams: PerDiemExpenseTransactionParams; participantParams: RequestMoneyParticipantParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; moneyRequestReportID?: string; }; type RequestMoneyInformation = { report: OnyxEntry; participantParams: RequestMoneyParticipantParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; gpsPoints?: GPSPoint; action?: IOUAction; reimbursible?: boolean; @@ -385,7 +386,7 @@ type MoneyRequestInformationParams = { parentChatReport: OnyxEntry; transactionParams: RequestMoneyTransactionParams; participantParams: RequestMoneyParticipantParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; moneyRequestReportID?: string; existingTransactionID?: string; existingTransaction?: OnyxEntry; @@ -422,7 +423,7 @@ type BuildOnyxDataForMoneyRequestParams = { shouldCreateNewMoneyRequestReport: boolean; isOneOnOneSplit?: boolean; existingTransactionThreadReportID?: string; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; optimisticParams: MoneyRequestOptimisticParams; }; @@ -449,7 +450,7 @@ type CreateDistanceRequestInformation = { iouType?: ValueOf; existingTransaction?: OnyxEntry; transactionParams: DistanceRequestTransactionParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; }; type TrackExpenseTransactionParams = { @@ -477,10 +478,68 @@ type CreateTrackExpenseParams = { isDraftPolicy: boolean; action?: IOUAction; participantParams: RequestMoneyParticipantParams; - policyParams?: RequestMoneyPolicyParams; + policyParams?: BasePolicyParams; transactionParams: TrackExpenseTransactionParams; }; +type BuildOnyxDataForInvoiceParams = { + chat: { + report: OnyxEntry; + createdAction: OptimisticCreatedReportAction; + reportPreviewAction: ReportAction; + isNewReport: boolean; + }; + iou: { + createdAction: OptimisticCreatedReportAction; + action: OptimisticIOUReportAction; + report: OnyxTypes.Report; + }; + transactionParams: { + transaction: OnyxTypes.Transaction; + threadReport: OptimisticChatReport; + threadCreatedReportAction: OptimisticCreatedReportAction | null; + }; + policyParams: BasePolicyParams; + optimisticData: { + recentlyUsedCurrencies?: string[]; + policyRecentlyUsedCategories: string[]; + policyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags; + personalDetailListAction: OnyxTypes.PersonalDetailsList; + }; + companyName?: string; + companyWebsite?: string; +}; + +type GetTrackExpenseInformationTransactionParams = { + comment: string; + amount: number; + currency: string; + created: string; + merchant: string; + receipt: OnyxEntry; + category?: string; + tag?: string; + taxCode?: string; + taxAmount?: number; + billable?: boolean; + linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction; +}; + +type GetTrackExpenseInformationParticipantParams = { + payeeEmail?: string; + payeeAccountID?: number; + participant: Participant; +}; + +type GetTrackExpenseInformationParams = { + parentChatReport: OnyxEntry; + moneyRequestReportID?: string; + existingTransactionID?: string; + participantParams: GetTrackExpenseInformationParticipantParams; + policyParams: BasePolicyParams; + transactionParams: GetTrackExpenseInformationTransactionParams; +}; + let allPersonalDetails: OnyxTypes.PersonalDetailsList = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, @@ -1462,36 +1521,18 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR } /** Builds the Onyx data for an invoice */ -function buildOnyxDataForInvoice( - chatReport: OnyxEntry, - iouReport: OnyxTypes.Report, - transaction: OnyxTypes.Transaction, - chatCreatedAction: OptimisticCreatedReportAction, - iouCreatedAction: OptimisticCreatedReportAction, - iouAction: OptimisticIOUReportAction, - optimisticPersonalDetailListAction: OnyxTypes.PersonalDetailsList, - reportPreviewAction: ReportAction, - optimisticPolicyRecentlyUsedCategories: string[], - optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, - isNewChatReport: boolean, - transactionThreadReport: OptimisticChatReport, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction | null, - policy?: OnyxEntry, - policyTagList?: OnyxEntry, - policyCategories?: OnyxEntry, - optimisticRecentlyUsedCurrencies?: string[], - companyName?: string, - companyWebsite?: string, -): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { - const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); +function buildOnyxDataForInvoice(invoiceParams: BuildOnyxDataForInvoiceParams): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { + const {chat, iou, transactionParams, policyParams, optimisticData: optimisticDataParams, companyName, companyWebsite} = invoiceParams; + + const clearedPendingFields = Object.fromEntries(Object.keys(transactionParams.transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`, value: { - ...iouReport, - lastMessageText: getReportActionText(iouAction), - lastMessageHtml: getReportActionHtml(iouAction), + ...iou.report, + lastMessageText: getReportActionText(iou.action), + lastMessageHtml: getReportActionHtml(iou.action), pendingFields: { createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, @@ -1499,96 +1540,96 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, - value: transaction, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`, + value: transactionParams.transaction, }, - isNewChatReport + chat.isNewReport ? { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`, value: { - [chatCreatedAction.reportActionID]: chatCreatedAction, - [reportPreviewAction.reportActionID]: reportPreviewAction, + [chat.createdAction.reportActionID]: chat.createdAction, + [chat.reportPreviewAction.reportActionID]: chat.reportPreviewAction, }, } : { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`, value: { - [reportPreviewAction.reportActionID]: reportPreviewAction, + [chat.reportPreviewAction.reportActionID]: chat.reportPreviewAction, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`, value: { - [iouCreatedAction.reportActionID]: iouCreatedAction as OnyxTypes.ReportAction, - [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + [iou.createdAction.reportActionID]: iou.createdAction as OnyxTypes.ReportAction, + [iou.action.reportActionID]: iou.action as OnyxTypes.ReportAction, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, - value: transactionThreadReport, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`, + value: transactionParams.threadReport, }, ]; - if (transactionThreadCreatedReportAction?.reportActionID) { + if (transactionParams.threadCreatedReportAction?.reportActionID) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + [transactionParams.threadCreatedReportAction.reportActionID]: transactionParams.threadCreatedReportAction, }, }); } const successData: OnyxUpdate[] = []; - if (chatReport) { + if (chat.report) { optimisticData.push({ // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page - onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + onyxMethod: chat.isNewReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report.reportID}`, value: { - ...chatReport, + ...chat.report, lastReadTime: DateUtils.getDBTime(), - iouReportID: iouReport.reportID, - ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), + iouReportID: iou.report?.reportID, + ...(chat.isNewReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }); } - if (optimisticPolicyRecentlyUsedCategories.length) { + if (optimisticDataParams.policyRecentlyUsedCategories.length) { optimisticData.push({ onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${iouReport.policyID}`, - value: optimisticPolicyRecentlyUsedCategories, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${iou.report?.policyID}`, + value: optimisticDataParams.policyRecentlyUsedCategories, }); } - if (optimisticRecentlyUsedCurrencies?.length) { + if (optimisticDataParams.recentlyUsedCurrencies?.length) { optimisticData.push({ onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.RECENTLY_USED_CURRENCIES, - value: optimisticRecentlyUsedCurrencies, + value: optimisticDataParams.recentlyUsedCurrencies, }); } - if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { + if (!isEmptyObject(optimisticDataParams.policyRecentlyUsedTags)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`, - value: optimisticPolicyRecentlyUsedTags, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iou.report?.policyID}`, + value: optimisticDataParams.policyRecentlyUsedTags, }); } const redundantParticipants: Record = {}; - if (!isEmptyObject(optimisticPersonalDetailListAction)) { + if (!isEmptyObject(optimisticDataParams.personalDetailListAction)) { const successPersonalDetailListAction: Record = {}; // BE will send different participants. We clear the optimistic ones to avoid duplicated entries - Object.keys(optimisticPersonalDetailListAction).forEach((accountIDKey) => { + Object.keys(optimisticDataParams.personalDetailListAction).forEach((accountIDKey) => { const accountID = Number(accountIDKey); successPersonalDetailListAction[accountID] = null; redundantParticipants[accountID] = null; @@ -1597,7 +1638,7 @@ function buildOnyxDataForInvoice( optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: optimisticPersonalDetailListAction, + value: optimisticDataParams.personalDetailListAction, }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -1609,7 +1650,7 @@ function buildOnyxDataForInvoice( successData.push( { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`, value: { participants: redundantParticipants, pendingFields: null, @@ -1618,14 +1659,14 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${iou.report?.reportID}`, value: { isOptimisticReport: false, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`, value: { participants: redundantParticipants, pendingFields: null, @@ -1634,14 +1675,14 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${transactionParams.threadReport.reportID}`, value: { isOptimisticReport: false, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`, value: { pendingAction: null, pendingFields: clearedPendingFields, @@ -1649,30 +1690,30 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chat.report?.reportID}`, value: { - ...(isNewChatReport + ...(chat.isNewReport ? { - [chatCreatedAction.reportActionID]: { + [chat.createdAction.reportActionID]: { pendingAction: null, errors: null, }, } : {}), - [reportPreviewAction.reportActionID]: { + [chat.reportPreviewAction.reportActionID]: { pendingAction: null, }, }, }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`, value: { - [iouCreatedAction.reportActionID]: { + [iou.createdAction.reportActionID]: { pendingAction: null, errors: null, }, - [iouAction.reportActionID]: { + [iou.action.reportActionID]: { pendingAction: null, errors: null, }, @@ -1680,12 +1721,12 @@ function buildOnyxDataForInvoice( }, ); - if (transactionThreadCreatedReportAction?.reportActionID) { + if (transactionParams.threadCreatedReportAction?.reportActionID) { successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { + [transactionParams.threadCreatedReportAction.reportActionID]: { pendingAction: null, errors: null, }, @@ -1693,11 +1734,11 @@ function buildOnyxDataForInvoice( }); } - if (isNewChatReport) { + if (chat.isNewReport) { successData.push( { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report?.reportID}`, value: { participants: redundantParticipants, pendingFields: null, @@ -1706,7 +1747,7 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${chat.report?.reportID}`, value: { isOptimisticReport: false, }, @@ -1719,13 +1760,13 @@ function buildOnyxDataForInvoice( const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${chat.report?.reportID}`, value: { - iouReportID: chatReport?.iouReportID, - lastReadTime: chatReport?.lastReadTime, + iouReportID: chat.report?.iouReportID, + lastReadTime: chat.report?.lastReadTime, pendingFields: null, - hasOutstandingChildRequest: chatReport?.hasOutstandingChildRequest, - ...(isNewChatReport + hasOutstandingChildRequest: chat.report?.hasOutstandingChildRequest, + ...(chat.isNewReport ? { errorFields: { createChat: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), @@ -1736,7 +1777,7 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${iou.report?.reportID}`, value: { pendingFields: null, errorFields: { @@ -1746,7 +1787,7 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionParams.threadReport.reportID}`, value: { errorFields: { createChat: getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage'), @@ -1755,7 +1796,7 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionParams.transaction.transactionID}`, value: { errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'), pendingFields: clearedPendingFields, @@ -1763,26 +1804,31 @@ function buildOnyxDataForInvoice( }, { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iou.report?.reportID}`, value: { - [iouCreatedAction.reportActionID]: { - // Disabling this line since transaction.filename can be an empty string - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, false, errorKey), + [iou.createdAction.reportActionID]: { + // Disabling this line since transactionParams.transaction.filename can be an empty string + errors: getReceiptError( + transactionParams.transaction.receipt, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + transactionParams.transaction?.filename || transactionParams.transaction.receipt?.filename, + false, + errorKey, + ), }, - [iouAction.reportActionID]: { + [iou.action.reportActionID]: { errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage'), }, }, }, ]; - if (transactionThreadCreatedReportAction?.reportActionID) { + if (transactionParams.threadCreatedReportAction?.reportActionID) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParams.threadReport.reportID}`, value: { - [transactionThreadCreatedReportAction.reportActionID]: { + [transactionParams.threadCreatedReportAction.reportActionID]: { errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericCreateInvoiceFailureMessage', errorKey), }, }, @@ -1792,7 +1838,7 @@ function buildOnyxDataForInvoice( if (companyName && companyWebsite) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`, value: { invoice: { companyName, @@ -1806,7 +1852,7 @@ function buildOnyxDataForInvoice( }); successData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`, value: { invoice: { pendingFields: { @@ -1818,7 +1864,7 @@ function buildOnyxDataForInvoice( }); failureData.push({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyParams.policy?.id}`, value: { invoice: { companyName: null, @@ -1833,17 +1879,17 @@ function buildOnyxDataForInvoice( } // We don't need to compute violations unless we're on a paid policy - if (!policy || !isPaidGroupPolicy(policy)) { + if (!policyParams.policy || !isPaidGroupPolicy(policyParams.policy)) { return [optimisticData, successData, failureData]; } const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( - transaction, + transactionParams.transaction, [], - policy, - policyTagList ?? {}, - policyCategories ?? {}, - hasDependentTags(policy, policyTagList ?? {}), + policyParams.policy, + policyParams.policyTagList ?? {}, + policyParams.policyCategories ?? {}, + hasDependentTags(policyParams.policy, policyParams.policyTagList ?? {}), true, ); @@ -1851,7 +1897,7 @@ function buildOnyxDataForInvoice( optimisticData.push(violationsOnyxData); failureData.push({ onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionParams.transaction.transactionID}`, value: [], }); } @@ -2579,27 +2625,24 @@ function getSendInvoiceInformation( ); // STEP 6: Build Onyx Data - const [optimisticData, successData, failureData] = buildOnyxDataForInvoice( - chatReport, - optimisticInvoiceReport, - optimisticTransaction, - optimisticCreatedActionForChat, - optimisticCreatedActionForIOUReport, - iouAction, - optimisticPersonalDetailListAction, - reportPreviewAction, - optimisticPolicyRecentlyUsedCategories, - optimisticPolicyRecentlyUsedTags, - isNewChatReport, - optimisticTransactionThread, - optimisticCreatedActionForTransactionThread, - policy, - policyTagList, - policyCategories, - optimisticRecentlyUsedCurrencies, + const [optimisticData, successData, failureData] = buildOnyxDataForInvoice({ + chat: {report: chatReport, createdAction: optimisticCreatedActionForChat, reportPreviewAction, isNewReport: isNewChatReport}, + iou: {createdAction: optimisticCreatedActionForIOUReport, action: iouAction, report: optimisticInvoiceReport}, + transactionParams: { + transaction: optimisticTransaction, + threadReport: optimisticTransactionThread, + threadCreatedReportAction: optimisticCreatedActionForTransactionThread, + }, + policyParams: {policy, policyTagList, policyCategories}, + optimisticData: { + personalDetailListAction: optimisticPersonalDetailListAction, + recentlyUsedCurrencies: optimisticRecentlyUsedCurrencies, + policyRecentlyUsedCategories: optimisticPolicyRecentlyUsedCategories, + policyRecentlyUsedTags: optimisticPolicyRecentlyUsedTags, + }, companyName, companyWebsite, - ); + }); return { createdIOUReportActionID: optimisticCreatedActionForIOUReport.reportActionID, @@ -3078,29 +3121,12 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI * Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead */ -function getTrackExpenseInformation( - parentChatReport: OnyxEntry, - participant: Participant, - comment: string, - amount: number, - currency: string, - created: string, - merchant: string, - receipt: OnyxEntry, - category: string | undefined, - tag: string | undefined, - taxCode: string | undefined, - taxAmount: number | undefined, - billable: boolean | undefined, - policy: OnyxEntry | undefined, - policyTagList: OnyxEntry | undefined, - policyCategories: OnyxEntry | undefined, - payeeEmail = currentUserEmail, - payeeAccountID = userAccountID, - moneyRequestReportID = '', - linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction, - existingTransactionID?: string, -): TrackExpenseInformation | null { +function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): TrackExpenseInformation | null { + const {parentChatReport, moneyRequestReportID = '', existingTransactionID, participantParams, policyParams, transactionParams} = params; + const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams; + const {policy, policyCategories, policyTagList} = policyParams; + const {comment, amount, currency, created, merchant, receipt, category, tag, taxCode, taxAmount, billable, linkedTrackedExpenseReportAction} = transactionParams; + const optimisticData: OnyxUpdate[] = []; const successData: OnyxUpdate[] = []; const failureData: OnyxUpdate[] = []; @@ -4824,31 +4850,38 @@ function trackExpense(params: CreateTrackExpenseParams) { actionableWhisperReportActionIDParam, onyxData, } = - getTrackExpenseInformation( - currentChatReport, - participant, - comment, - amount, - currency, - created, - merchant, - trackedReceipt, - category, - tag, - taxCode, - taxAmount, - billable, - policy, - policyTagList, - policyCategories, - payeeEmail, - payeeAccountID, + getTrackExpenseInformation({ + parentChatReport: currentChatReport, moneyRequestReportID, - linkedTrackedExpenseReportAction, - isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction) - ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID - : undefined, - ) ?? {}; + existingTransactionID: + isMovingTransactionFromTrackExpense && linkedTrackedExpenseReportAction && isMoneyRequestAction(linkedTrackedExpenseReportAction) + ? getOriginalMessage(linkedTrackedExpenseReportAction)?.IOUTransactionID + : undefined, + participantParams: { + participant, + payeeAccountID, + payeeEmail, + }, + transactionParams: { + comment, + amount, + currency, + created, + merchant, + receipt: trackedReceipt, + category, + tag, + taxCode, + taxAmount, + billable, + linkedTrackedExpenseReportAction, + }, + policyParams: { + policy, + policyCategories, + policyTagList, + }, + }) ?? {}; const activeReportID = isMoneyRequestReport ? report.reportID : chatReport?.reportID; const recentServerValidatedWaypoints = getRecentWaypoints().filter((item) => !item.pendingAction); @@ -5288,7 +5321,7 @@ function createSplitsAndOnyxData( // or, if the split is being made from the workspace chat, then the oneOnOneChatReport is the same as the splitChatReport // in this case existingSplitChatReport will belong to the policy expense chat and we won't be // entering code that creates optimistic personal details - if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat) { + if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat || isOneOnOneChat(splitChatReport)) { oneOnOneChatReport = splitChatReport; shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists; } else { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index d6910ebcb26b..63448c89fc8a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3643,15 +3643,14 @@ function prepareOnboardingOnyxData( userReportedIntegration?: OnboardingAccounting, wasInvited?: boolean, ) { - // If the user has the "combinedTrackSubmit" beta enabled we'll show different tasks for track and submit expense. if (engagementChoice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) { // eslint-disable-next-line no-param-reassign - data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]; + data = CONST.CREATE_EXPENSE_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]; } if (engagementChoice === CONST.ONBOARDING_CHOICES.EMPLOYER || engagementChoice === CONST.ONBOARDING_CHOICES.SUBMIT) { // eslint-disable-next-line no-param-reassign - data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT]; + data = CONST.CREATE_EXPENSE_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT]; } // Guides are assigned and tasks are posted in the #admins room for the MANAGE_TEAM and TRACK_WORKSPACE onboarding actions, except for emails that have a '+'. diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 574225b20f38..d403cc1c3108 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -30,6 +30,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {isOffline} from '@libs/Network/NetworkStore'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import NetworkConnection from '@libs/NetworkConnection'; import * as NumberUtils from '@libs/NumberUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import Pusher from '@libs/Pusher'; @@ -915,6 +916,7 @@ function subscribeToPusherPong() { // When any PONG event comes in, reset this flag so that checkforLatePongReplies will resume looking for missed PONGs pongHasBeenMissed = false; + NetworkConnection.setOfflineStatus(false, 'PONG event was recieved from the server so assuming that means the client is back online'); }); } @@ -938,6 +940,9 @@ function pingPusher() { const pingID = NumberUtils.rand64(); const pingTimestamp = Date.now(); + // Reset this flag so that checkforLatePongReplies will resume looking for missed PONGs (in the case we are coming back online after being offline for a bit) + pongHasBeenMissed = false; + // In local development, there can end up being multiple intervals running because when JS code is replaced with hot module replacement, the old interval is not cleared // and keeps running. This little bit of logic will attempt to keep multiple pings from happening. if (pingTimestamp - lastPingSentTimestamp < PING_INTERVAL_LENGTH_IN_SECONDS * 1000) { @@ -972,6 +977,7 @@ function checkforLatePongReplies() { // When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh lastPingSentTimestamp = Date.now(); pongHasBeenMissed = true; + NetworkConnection.setOfflineStatus(true, 'PONG event was not received from the server in time so assuming that means the client is offline'); } else { Log.info(`[Pusher PINGPONG] Last PONG event was ${timeSinceLastPongReceived} ms ago so not going offline`); } @@ -980,6 +986,11 @@ function checkforLatePongReplies() { let pingPusherIntervalID: ReturnType; let checkforLatePongRepliesIntervalID: ReturnType; function initializePusherPingPong() { + // Skip doing the ping pong during tests so that the network isn't knocked offline, forcing tests to timeout + if (process.env.NODE_ENV === 'test') { + return; + } + // Only run the ping pong from the leader client if (!ActiveClientManager.isClientTheLeader()) { Log.info("[Pusher PINGPONG] Not starting PING PONG because this instance isn't the leader client"); diff --git a/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx index b75cc1045525..67430030a5f8 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Country/subSteps/Confirmation.tsx @@ -46,7 +46,7 @@ function Confirmation({onNext}: SubStepProps) { return; } - Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_OVERVIEW.getRoute(policyID)); }; const handleSelectingCountry = (country: string) => { diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx index a4013e0795a5..6d917e052189 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersCardPage.tsx @@ -11,12 +11,14 @@ import CardListItem from '@components/SelectionList/Search/CardListItem'; import type {AdditionalCardProps} from '@components/SelectionList/Search/CardListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import {openSearchFiltersCardPage, updateAdvancedFilters} from '@libs/actions/Search'; import {getBankName, getCardFeedIcon, isCard, isCardClosed, isCardHiddenFromSearch} from '@libs/CardUtils'; import {getDescriptionForPolicyDomainCard, getPolicy} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; +import type IllustrationsType from '@styles/theme/illustrations/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -43,10 +45,10 @@ function getRepeatingBanks(workspaceCardFeedsKeys: string[], domainFeedsData: Re return Object.keys(bankFrequency).filter((bank) => bankFrequency[bank] > 1); } -function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[]): CardFilterItem { +function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsList, selectedCards: string[], illustrations: IllustrationsType): CardFilterItem { const personalDetails = personalDetailsList[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; const isSelected = selectedCards.includes(card.cardID.toString()); - const icon = getCardFeedIcon(card?.bank as CompanyCardFeed); + const icon = getCardFeedIcon(card?.bank as CompanyCardFeed, illustrations); const cardName = card?.nameValuePairs?.cardTitle; const text = personalDetails?.displayName ?? cardName; @@ -71,13 +73,14 @@ function buildCardsData( userCardList: CardList, personalDetailsList: PersonalDetailsList, selectedCards: string[], + illustrations: IllustrationsType, isClosedCards = false, ): ItemsGroupedBySelection { // Filter condition to build different cards data for closed cards and individual cards based on the isClosedCards flag, we don't want to show closed cards in the individual cards section const filterCondition = (card: Card) => (isClosedCards ? isCardClosed(card) : !isCardHiddenFromSearch(card) && !isCardClosed(card)); const userAssignedCards: CardFilterItem[] = Object.values(userCardList ?? {}) .filter((card) => filterCondition(card)) - .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards)); + .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards, illustrations)); // When user is admin of a workspace he sees all the cards of workspace under cards_ Onyx key const allWorkspaceCards: CardFilterItem[] = Object.values(workspaceCardFeeds) @@ -85,7 +88,7 @@ function buildCardsData( .flatMap((cardFeed) => { return Object.values(cardFeed as Record) .filter((card) => card && isCard(card) && !userCardList?.[card.cardID] && filterCondition(card)) - .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards)); + .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards, illustrations)); }); const allCardItems = [...userAssignedCards, ...allWorkspaceCards]; @@ -108,6 +111,7 @@ function createCardFeedItem({ correspondingCardIDs, selectedCards, translate, + illustrations, }: { bank: string; cardFeedLabel: string | undefined; @@ -115,12 +119,16 @@ function createCardFeedItem({ correspondingCardIDs: string[]; selectedCards: string[]; translate: LocaleContextProps['translate']; + illustrations: IllustrationsType; }): CardFilterItem { const cardFeedBankName = bank === CONST.EXPENSIFY_CARD.BANK ? translate('search.filters.card.expensify') : getBankName(bank as CompanyCardFeed); - const text = translate('search.filters.card.cardFeedName', {cardFeedBankName, cardFeedLabel}); + const text = + cardFeedBankName === CONST.COMPANY_CARDS.CARD_TYPE.CSV + ? translate('search.filters.card.cardFeedNameCSV', {cardFeedLabel}) + : translate('search.filters.card.cardFeedName', {cardFeedBankName, cardFeedLabel}); const isSelected = correspondingCardIDs.every((card) => selectedCards.includes(card)); - const icon = getCardFeedIcon(bank as CompanyCardFeed); + const icon = getCardFeedIcon(bank as CompanyCardFeed, illustrations); return { text, keyForList, @@ -139,6 +147,7 @@ function buildCardFeedsData( domainFeedsData: Record, selectedCards: string[], translate: LocaleContextProps['translate'], + illustrations: IllustrationsType, ): ItemsGroupedBySelection { const repeatingBanks = getRepeatingBanks(Object.keys(workspaceCardFeeds), domainFeedsData); const selectedFeeds: CardFilterItem[] = []; @@ -155,6 +164,7 @@ function buildCardFeedsData( translate, keyForList: `${domainName}-${bank}`, selectedCards, + illustrations, }); if (feedItem.isSelected) { selectedFeeds.push(feedItem); @@ -187,6 +197,7 @@ function buildCardFeedsData( translate, keyForList: cardFeedKey, selectedCards, + illustrations, }); if (feedItem.isSelected) { selectedFeeds.push(feedItem); @@ -201,6 +212,7 @@ function buildCardFeedsData( function SearchFiltersCardPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); + const illustrations = useThemeIllustrations(); const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST); const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); @@ -215,13 +227,13 @@ function SearchFiltersCardPage() { }, []); const individualCardsSectionData = useMemo( - () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, false), - [workspaceCardFeeds, userCardList, personalDetails, selectedCards], + () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, illustrations, false), + [workspaceCardFeeds, userCardList, personalDetails, selectedCards, illustrations], ); const closedCardsSectionData = useMemo( - () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, true), - [workspaceCardFeeds, userCardList, personalDetails, selectedCards], + () => buildCardsData(workspaceCardFeeds ?? {}, userCardList ?? {}, personalDetails ?? {}, selectedCards, illustrations, true), + [workspaceCardFeeds, userCardList, personalDetails, selectedCards, illustrations], ); const domainFeedsData = useMemo( @@ -241,8 +253,8 @@ function SearchFiltersCardPage() { ); const cardFeedsSectionData = useMemo( - () => buildCardFeedsData(workspaceCardFeeds ?? {}, domainFeedsData, selectedCards, translate), - [domainFeedsData, workspaceCardFeeds, selectedCards, translate], + () => buildCardFeedsData(workspaceCardFeeds ?? {}, domainFeedsData, selectedCards, translate, illustrations), + [domainFeedsData, workspaceCardFeeds, selectedCards, translate, illustrations], ); const shouldShowSearchInput = diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index e4b8ddc8b45c..f01be69fd273 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -177,6 +177,7 @@ function SearchTypeMenu({queryJSON, shouldGroupByReports}: SearchTypeMenuProps) {typeMenuItems.map((item, index) => { diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 73b1a4af8ec1..763ee3b10039 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -1,5 +1,5 @@ import {useRoute} from '@react-navigation/native'; -import React, {memo, useEffect, useMemo} from 'react'; +import React, {memo, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -111,6 +111,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL); const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`); + const [isDismissedDiscountBanner, setIsDismissedDiscountBanner] = useState(false); const {translate} = useLocalize(); const theme = useTheme(); @@ -203,21 +204,23 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, onboardingPurposeSelected); + const shouldShowEarlyDiscountBanner = shouldShowDiscount && isChatUsedForOnboarding; + const shouldShowGuideBookingButtonInEarlyDiscountBanner = shouldShowGuideBooking && shouldShowEarlyDiscountBanner && !isDismissedDiscountBanner; const guideBookingButton = (