diff --git a/.github/scripts/checkParser.sh b/.github/scripts/checkParser.sh
new file mode 100755
index 000000000000..d63f4a01452f
--- /dev/null
+++ b/.github/scripts/checkParser.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+set -e
+
+ROOT_DIR=$(dirname "$(dirname "$(dirname "${BASH_SOURCE[0]}")")")
+cd "$ROOT_DIR" || exit 1
+
+autocomplete_parser_backup="src/libs/SearchParser/autocompleteParser.js.bak"
+search_parser_backup="src/libs/SearchParser/searchParser.js.bak"
+
+#Copying the current .js parser files
+cp src/libs/SearchParser/autocompleteParser.js "$autocomplete_parser_backup" 2>/dev/null
+cp src/libs/SearchParser/searchParser.js "$search_parser_backup" 2>/dev/null
+
+#Running the scripts that generate the .js parser files
+npm run generate-search-parser
+npm run generate-autocomplete-parser
+
+#Checking if the saved files differ from the newly generated
+if ! diff -q "$autocomplete_parser_backup" src/libs/SearchParser/autocompleteParser.js >/dev/null ||
+ ! diff -q "$search_parser_backup" src/libs/SearchParser/searchParser.js >/dev/null; then
+ echo "The files generated from the .peggy files using the commands: generate-search-parser and generate-autocomplete-parser are not identical to those currently on this branch."
+ echo "The parser .js files should never be edited manually. Make sure you’ve run locally: npm run generate-search-parser and npm run generate-autocomplete-parser, and committed the changes."
+ exit 1
+else
+ echo "The files generated from the .peggy files using the commands: generate-search-parser and generate-autocomplete-parser are identical to those currently on this branch."
+ exit 0
+fi
diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml
index bfb7e2b0a813..5cb0a99730c9 100644
--- a/.github/workflows/cherryPick.yml
+++ b/.github/workflows/cherryPick.yml
@@ -86,14 +86,13 @@ jobs:
if git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}; then
echo "🎉 No conflicts! CP was a success, PR can be automerged 🎉"
echo "HAS_CONFLICTS=false" >> "$GITHUB_OUTPUT"
+ git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})"
else
echo "😞 PR can't be automerged, there are merge conflicts in the following files:"
git --no-pager diff --name-only --diff-filter=U
- git add .
- GIT_MERGE_AUTOEDIT=no git cherry-pick --continue
+ git cherry-pick --abort
echo "HAS_CONFLICTS=true" >> "$GITHUB_OUTPUT"
fi
- git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by ${{ github.actor }})"
- name: Push changes
run: |
@@ -110,7 +109,25 @@ jobs:
run: |
gh pr create \
--title "🍒 Cherry pick PR #${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒" \
- --body "🍒 Cherry pick https://github.com/Expensify/App/pull/${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒" \
+ --body \
+ "🍒 Cherry pick https://github.com/Expensify/App/pull/${{ github.event.inputs.PULL_REQUEST_NUMBER }} to staging 🍒
+ This PR had conflicts when we tried to cherry-pick it to staging. You'll need to manually perform the cherry-pick, using the following steps:
+
+ \`\`\`bash
+ git fetch
+ git checkout ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }}-${{ github.run_attempt }}
+ git cherry-pick -S -x --mainline 1 ${{ steps.getCPMergeCommit.outputs.MERGE_COMMIT_SHA }}
+ \`\`\`
+
+ Then manually resolve conflicts, and commit the change with \`git cherry-pick --continue\`. Lastly, please run:
+
+ \`\`\`bash
+ git commit --amend -m \"$(git log -1 --pretty=%B)\" -m \"(CP triggered by ${{ github.actor }})\"
+ \`\`\`
+
+ That will help us keep track of who triggered this CP. Once all that's done, push your changes with \`git push origin ${{ github.actor }}-cherry-pick-staging-${{ github.event.inputs.PULL_REQUEST_NUMBER }}-${{ github.run_attempt }}\`, and then open this PR for review.
+
+ Note that you **must** test this PR, and both the author and reviewer checklist should be completed, just as if you were merging the PR to main." \
--label "Engineering,Hourly" \
--base "staging"
sleep 5
@@ -118,11 +135,12 @@ jobs:
"This pull request has merge conflicts and can not be automatically merged. :disappointed:
Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge.
**Important:** There may be conflicts that GitHub is not able to detect, so please _carefully_ review this pull request before approving."
- gh pr edit --add-assignee "${{ github.actor }},${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }}"
+ ORIGINAL_PR_AUTHOR="$(gh pr view ${{ github.event.inputs.PULL_REQUEST_NUMBER }} --json author --jq .author.login)"
+ gh pr edit --add-assignee "${{ github.actor }},${{ steps.getCPMergeCommit.outputs.MERGE_ACTOR }},$ORIGINAL_PR_AUTHOR"
env:
GITHUB_TOKEN: ${{ steps.setupGitForOSBotify.outputs.OS_BOTIFY_API_TOKEN }}
- - name: Label PR with CP Staging
+ - name: Label original PR with CP Staging
run: gh pr edit ${{ inputs.PULL_REQUEST_NUMBER }} --add-label 'CP Staging'
env:
GITHUB_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/verifyParserFiles.yml b/.github/workflows/verifyParserFiles.yml
new file mode 100644
index 000000000000..66fec63f40f8
--- /dev/null
+++ b/.github/workflows/verifyParserFiles.yml
@@ -0,0 +1,22 @@
+name: Check consistency of search parser files
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore: [staging, production]
+ paths:
+ - "src/libs/SearchParser/**"
+
+jobs:
+ verify:
+ if: github.actor != 'OSBotify' && github.actor != 'imgbot[bot]'
+ runs-on: macos-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: ./.github/actions/composite/setupNode
+
+ - name: Verify parser files consistency
+ run: ./.github/scripts/checkParser.sh
diff --git a/Mobile-Expensify b/Mobile-Expensify
index 42e14b319cec..6a1bd2e5c803 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 42e14b319cece1e7e238cd72bc49bf8aed2fb5e5
+Subproject commit 6a1bd2e5c80336edaf5ea929ce30102cfa3ef65b
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f31731fc9806..25c5cf9cfc7d 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009009302
- versionName "9.0.93-2"
+ versionCode 1009009303
+ versionName "9.0.93-3"
// 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/customEmoji/global-create.svg b/assets/images/customEmoji/global-create.svg
new file mode 100644
index 000000000000..60b46eb97aed
--- /dev/null
+++ b/assets/images/customEmoji/global-create.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index f76a78dc7973..0e72054a1a83 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -44,7 +44,7 @@
CFBundleVersion
- 9.0.93.2
+ 9.0.93.3
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 6da23735886e..58532a22ad85 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.93.2
+ 9.0.93.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index bdf529314d4c..2ac74ffa3390 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.93
CFBundleVersion
- 9.0.93.2
+ 9.0.93.3
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index bf7bc2644b14..1147dbf592ef 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.93-2",
+ "version": "9.0.93-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.93-2",
+ "version": "9.0.93-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 67659c0462e5..88ec87bff8d0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.93-2",
+ "version": "9.0.93-3",
"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 27914bb44e42..20966b930f2e 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -75,6 +75,7 @@ const selectableOnboardingChoices = {
const backendOnboardingChoices = {
ADMIN: 'newDotAdmin',
SUBMIT: 'newDotSubmit',
+ TRACK_WORKSPACE: 'newDotTrackWorkspace',
} as const;
const onboardingChoices = {
@@ -101,6 +102,50 @@ const selfGuidedTourTask: OnboardingTask = {
description: ({navatticURL}) => `[Take a self-guided product tour](${navatticURL}) and learn about everything Expensify has to offer.`,
};
+const createWorkspaceTask: OnboardingTask = {
+ type: 'createWorkspace',
+ autoCompleted: true,
+ title: 'Create a workspace',
+ description: ({workspaceSettingsLink}) =>
+ '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' +
+ '\n' +
+ 'Here’s how to create a workspace:\n' +
+ '\n' +
+ '1. Click *Settings*.\n' +
+ '2. Click *Workspaces* > *New workspace*.\n' +
+ '\n' +
+ `*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,
+ title: 'Set up categories',
+ description: ({workspaceCategoriesLink}) =>
+ '*Set up categories* so your team can code expenses for easy reporting.\n' +
+ '\n' +
+ 'Here’s how to set up categories:\n' +
+ '\n' +
+ '1. Click *Settings*.\n' +
+ '2. Go to *Workspaces*.\n' +
+ '3. Select your workspace.\n' +
+ '4. Click *Categories*.\n' +
+ "5. Disable any categories you don't need.\n" +
+ '6. Add your own categories in the top right.\n' +
+ '\n' +
+ `[Take me to workspace category settings](${workspaceCategoriesLink}).`,
+};
+
const onboardingEmployerOrSubmitMessage: OnboardingMessage = {
message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.',
tasks: [
@@ -114,7 +159,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = {
'\n' +
'Here’s how to submit an expense:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button.\n' +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Add your reimburser to the request.\n' +
@@ -137,7 +182,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage =
'\n' +
'Here’s how to submit an expense:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button\n' +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Add your reimburser to the request.\n' +
@@ -161,7 +206,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = {
'\n' +
'Here’s how to track an expense:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button.\n' +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Click "Just track it (don\'t submit it)".\n' +
@@ -184,7 +229,7 @@ const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = {
'\n' +
'Here’s how to track an expense:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button.\n' +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Click "Just track it (don\'t submit it)".\n' +
@@ -5009,30 +5054,9 @@ const CONST = {
[onboardingChoices.MANAGE_TEAM]: {
message: 'Here are some important tasks to help get your team’s expenses under control.',
tasks: [
- {
- type: 'createWorkspace',
- autoCompleted: true,
- title: 'Create a workspace',
- description: ({workspaceSettingsLink}) =>
- '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' +
- '\n' +
- 'Here’s how to create a workspace:\n' +
- '\n' +
- '1. Click *Settings*.\n' +
- '2. Click *Workspaces* > *New workspace*.\n' +
- '\n' +
- `*Your new workspace is ready!* [Check it out](${workspaceSettingsLink}).`,
- },
+ createWorkspaceTask,
selfGuidedTourTask,
- {
- 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}).`,
- },
+ meetGuideTask,
{
type: 'setupCategoriesAndTags',
autoCompleted: false,
@@ -5042,24 +5066,7 @@ const CONST = {
'\n' +
`Import them automatically by [connecting your accounting software](${workspaceAccountingLink}), or set them up manually in your [workspace settings](${workspaceSettingsLink}).`,
},
- {
- type: 'setupCategories',
- autoCompleted: false,
- title: 'Set up categories',
- description: ({workspaceCategoriesLink}) =>
- '*Set up categories* so your team can code expenses for easy reporting.\n' +
- '\n' +
- 'Here’s how to set up categories:\n' +
- '\n' +
- '1. Click *Settings*.\n' +
- '2. Go to *Workspaces*.\n' +
- '3. Select your workspace.\n' +
- '4. Click *Categories*.\n' +
- "5. Disable any categories you don't need.\n" +
- '6. Add your own categories in the top right.\n' +
- '\n' +
- `[Take me to workspace category settings](${workspaceCategoriesLink}).`,
- },
+ setupCategoriesTask,
{
type: 'setupTags',
autoCompleted: false,
@@ -5137,6 +5144,42 @@ const CONST = {
},
],
},
+ [onboardingChoices.TRACK_WORKSPACE]: {
+ message: 'Here are some important tasks to help get your workspace set up.',
+ video: {
+ url: `${CLOUDFRONT_URL}/videos/guided-setup-manage-team-v2.mp4`,
+ thumbnailUrl: `${CLOUDFRONT_URL}/images/guided-setup-manage-team.jpg`,
+ duration: 55,
+ width: 1280,
+ height: 960,
+ },
+ tasks: [
+ createWorkspaceTask,
+ meetGuideTask,
+ setupCategoriesTask,
+ {
+ type: 'inviteAccountant',
+ autoCompleted: false,
+ title: 'Invite your accountant',
+ description: ({workspaceMembersLink}) =>
+ '*Invite your accountant* to Expensify and share your expenses with them to make tax time easier.\n' +
+ '\n' +
+ 'Here’s how to invite your accountant:\n' +
+ '\n' +
+ '1. Click your profile picture.\n' +
+ '2. Go to *Workspaces*.\n' +
+ '3. Select your workspace.\n' +
+ '4. Click *Members* > Invite member.\n' +
+ '5. Enter their email or phone number.\n' +
+ '6. Add an invite message if you’d like.\n' +
+ '7. You’ll be set as the expense approver. You can change this to any admin once you invite your team.\n' +
+ '\n' +
+ 'That’s it, happy expensing! 😄\n' +
+ '\n' +
+ `[View your workspace members](${workspaceMembersLink}).`,
+ },
+ ],
+ },
[onboardingChoices.PERSONAL_SPEND]: onboardingPersonalSpendMessage,
[onboardingChoices.CHAT_SPLIT]: {
message: 'Splitting bills with friends is as easy as sending a message. Here’s how.',
@@ -5151,7 +5194,7 @@ const CONST = {
'\n' +
'Here’s how to start a chat:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button.\n' +
'2. Choose *Start chat*.\n' +
'3. Enter emails or phone numbers.\n' +
'\n' +
@@ -5168,7 +5211,7 @@ const CONST = {
'\n' +
'Here’s how to request money:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button\n' +
'2. Choose *Start chat*.\n' +
'3. Enter any email, SMS, or name of who you want to split with.\n' +
'4. From within the chat, click the *+* button on the message bar, and click *Split expense*.\n' +
@@ -5181,15 +5224,7 @@ const CONST = {
[onboardingChoices.ADMIN]: {
message: "As an admin, learn how to manage your team's workspace and submit expenses yourself.",
tasks: [
- {
- type: 'meetSetupSpecialist',
- autoCompleted: false,
- title: 'Meet your setup specialist',
- description:
- '*Meet your setup specialist* who can answer any questions as you get started with Expensify. Yes, a real human!' +
- '\n' +
- 'Chat with them in your #admins room or schedule a call today.',
- },
+ meetGuideTask,
{
type: 'reviewWorkspaceSettings',
autoCompleted: false,
@@ -5211,7 +5246,7 @@ const CONST = {
'\n' +
'Here’s how to submit an expense:\n' +
'\n' +
- '1. Click the green *+* button.\n' +
+ '1. Press the button.\n' +
'2. Choose *Create expense*.\n' +
'3. Enter an amount or scan a receipt.\n' +
'4. Add your reimburser to the request.\n' +
@@ -5978,6 +6013,13 @@ const CONST = {
TRAIN: 'train',
},
+ CANCELLATION_POLICY: {
+ UNKNOWN: 'UNKNOWN',
+ NON_REFUNDABLE: 'NON_REFUNDABLE',
+ FREE_CANCELLATION_UNTIL: 'FREE_CANCELLATION_UNTIL',
+ PARTIALLY_REFUNDABLE: 'PARTIALLY_REFUNDABLE',
+ },
+
DOT_SEPARATOR: '•',
DEFAULT_TAX: {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 5e0276e9d8cd..54b7da704cd1 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -548,6 +548,9 @@ const ONYXKEYS = {
/** Expensify cards settings */
PRIVATE_EXPENSIFY_CARD_SETTINGS: 'private_expensifyCardSettings_',
+ /** Expensify cards manual billing setting */
+ PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING: 'private_expensifyCardManualBilling_',
+
/** Stores which connection is set up to use Continuous Reconciliation */
EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'expensifyCard_continuousReconciliationConnection_',
@@ -898,6 +901,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER]: OnyxTypes.CardFeeds;
[ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
+ [ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING]: boolean;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c4f266ae2590..393085ab4384 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -526,7 +526,7 @@ const ROUTES = {
},
MONEY_REQUEST_STEP_CATEGORY: {
route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?',
- getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string | undefined, backTo = '', reportActionID?: string) =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_ATTENDEE: {
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index 22d26b4f264d..27a04c4dde4e 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -85,6 +85,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
contentModel: HTMLContentModel.textual,
mixedUAStyles: {...styles.taskTitleMenuItem},
}),
+ 'custom-emoji': HTMLElementModel.fromCustomModel({tagName: 'custom-emoji', contentModel: HTMLContentModel.textual}),
'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
diff --git a/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx
new file mode 100644
index 000000000000..8cd33eab6c90
--- /dev/null
+++ b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx
@@ -0,0 +1,21 @@
+import type {ReactNode} from 'react';
+import React from 'react';
+import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
+
+type CustomEmojiWithDefaultPressableActionProps = {
+ /* Key name identifying the emoji */
+ emojiKey: string;
+
+ /* Emoji content to render */
+ children: ReactNode;
+};
+
+function CustomEmojiWithDefaultPressableAction({emojiKey, children}: CustomEmojiWithDefaultPressableActionProps) {
+ if (emojiKey === 'actionMenuIcon') {
+ return {children};
+ }
+
+ return children;
+}
+
+export default CustomEmojiWithDefaultPressableAction;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx
new file mode 100644
index 000000000000..dab8c89013dd
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import type {FC} from 'react';
+import {View} from 'react-native';
+import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
+import type {SvgProps} from 'react-native-svg';
+import GlobalCreateIcon from '@assets/images/customEmoji/global-create.svg';
+import CustomEmojiWithDefaultPressableAction from '@components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction';
+import ImageSVG from '@components/ImageSVG';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+
+const emojiMap: Record> = {
+ actionMenuIcon: GlobalCreateIcon,
+};
+
+function CustomEmojiRenderer({tnode}: CustomRendererProps) {
+ const styles = useThemeStyles();
+ const emojiKey = tnode.attributes.emoji;
+
+ if (emojiMap[emojiKey]) {
+ const image = (
+
+
+
+ );
+
+ if ('pressablewithdefaultaction' in tnode.attributes) {
+ return {image};
+ }
+
+ return image;
+ }
+
+ return null;
+}
+
+export default CustomEmojiRenderer;
+export {emojiMap};
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
index 6ff5b6105c9f..703f605d1069 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts
@@ -1,6 +1,7 @@
import type {CustomTagRendererRecord} from 'react-native-render-html';
import AnchorRenderer from './AnchorRenderer';
import CodeRenderer from './CodeRenderer';
+import CustomEmojiRenderer from './CustomEmojiRenderer';
import DeletedActionRenderer from './DeletedActionRenderer';
import EditedRenderer from './EditedRenderer';
import EmojiRenderer from './EmojiRenderer';
@@ -30,6 +31,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
'mention-user': MentionUserRenderer,
'mention-report': MentionReportRenderer,
'mention-here': MentionHereRenderer,
+ 'custom-emoji': CustomEmojiRenderer,
emoji: EmojiRenderer,
'next-step-email': NextStepEmailRenderer,
'deleted-action': DeletedActionRenderer,
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index 894f5ddc2477..e48646204f34 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import LottieAnimations from '@components/LottieAnimations';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
import TextBlock from '@components/TextBlock';
+import useLHNEstimatedListSize from '@hooks/useLHNEstimatedListSize';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -48,6 +49,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
const styles = useThemeStyles();
const {translate, preferredLocale} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const estimatedListSize = useLHNEstimatedListSize();
const shouldShowEmptyLHN = shouldUseNarrowLayout && data.length === 0;
// When the first item renders we want to call the onFirstItemRendered callback.
@@ -284,6 +286,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio
showsVerticalScrollIndicator={false}
onLayout={onLayout}
onScroll={onScroll}
+ estimatedListSize={estimatedListSize}
/>
)}
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 604e2b3065fd..1e7a9f796641 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -30,7 +30,7 @@ import Performance from '@libs/Performance';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import {
isAdminRoom,
- isChatUsedForOnboarding,
+ isChatUsedForOnboarding as isChatUsedForOnboardingReportUtils,
isConciergeChatReport,
isGroupChat,
isOneOnOneChat,
@@ -62,7 +62,8 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const session = useSession();
const shouldShowWokspaceChatTooltip = isPolicyExpenseChat(report) && activePolicyID === report?.policyID && session?.accountID === report?.ownerAccountID;
const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+');
- const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) : isConciergeChatReport(report);
+ const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, introSelected?.choice);
+ const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? isAdminRoom(report) && isChatUsedForOnboarding : isConciergeChatReport(report);
const isActiveRouteHome = useIsCurrentRouteHome();
const {tooltipToRender, shouldShowTooltip} = useMemo(() => {
@@ -76,9 +77,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip);
- // During the onboarding flow, the introSelected NVP is not yet available.
- const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
-
const {translate} = useLocalize();
const [isContextMenuActive, setIsContextMenuActive] = useState(false);
@@ -283,7 +281,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
isSystemChat(report)
}
/>
- {isChatUsedForOnboarding(report, onboardingPurposeSelected) && }
+ {isChatUsedForOnboarding && }
{isStatusVisible && (
- {reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName}
+ {reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : Str.recapitalize(reservation.start.longName ?? '')}
{!!bottomDescription && {bottomDescription}}
diff --git a/src/components/ReportActionItem/TripRoomPreview.tsx b/src/components/ReportActionItem/TripRoomPreview.tsx
index d85c19d21ee0..de8a559c602a 100644
--- a/src/components/ReportActionItem/TripRoomPreview.tsx
+++ b/src/components/ReportActionItem/TripRoomPreview.tsx
@@ -1,3 +1,4 @@
+import {Str} from 'expensify-common';
import React, {useMemo} from 'react';
import type {ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
import {FlatList, View} from 'react-native';
@@ -53,16 +54,17 @@ type TripRoomPreviewProps = {
type ReservationViewProps = {
reservation: Reservation;
+ onPress?: () => void;
};
-function ReservationView({reservation}: ReservationViewProps) {
+function ReservationView({reservation, onPress}: ReservationViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const reservationIcon = getTripReservationIcon(reservation.type);
- const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : reservation.start.longName;
+ const title = reservation.type === CONST.RESERVATION_TYPE.CAR ? reservation.carInfo?.name : Str.recapitalize(reservation.start.longName ?? '');
let titleComponent = (
) => ;
-
function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -136,6 +137,14 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch
);
}, [currency, totalDisplaySpend, tripTransactions]);
+ const navigateToTrip = () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID));
+ const renderItem = ({item}: ListRenderItemInfo) => (
+
+ );
+
return (
canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)}
shouldUseHapticsOnLongPress
- style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox, styles.cursorDefault]}
+ style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('iou.viewDetails')}
>
@@ -184,7 +194,7 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch
diff --git a/src/hooks/useLHNEstimatedListSize/index.native.ts b/src/hooks/useLHNEstimatedListSize/index.native.ts
new file mode 100644
index 000000000000..a26e7a8f65eb
--- /dev/null
+++ b/src/hooks/useLHNEstimatedListSize/index.native.ts
@@ -0,0 +1,18 @@
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import variables from '@styles/variables';
+import type UseLHNEstimatedListSize from './types';
+
+/**
+ * This a native specific implementation for FlashList of LHNOptionsList. It calculates estimated visible height and width of the list. It is not the scroll content size. Defining this prop will enable the list to be rendered immediately. Without it, the list first needs to measure its size, leading to a small delay during the first render.
+ */
+const useLHNEstimatedListSize: UseLHNEstimatedListSize = () => {
+ const {windowHeight, windowWidth} = useWindowDimensions();
+ const listHeight = windowHeight - variables.bottomTabHeight;
+
+ return {
+ height: listHeight,
+ width: windowWidth,
+ };
+};
+
+export default useLHNEstimatedListSize;
diff --git a/src/hooks/useLHNEstimatedListSize/index.ts b/src/hooks/useLHNEstimatedListSize/index.ts
new file mode 100644
index 000000000000..d9b5cb9d3ec0
--- /dev/null
+++ b/src/hooks/useLHNEstimatedListSize/index.ts
@@ -0,0 +1,8 @@
+import type UseLHNEstimatedListSize from './types';
+
+/**
+ * This is the web implementation. It is intentionally left unimplemented because it does not function correctly on the web.
+ */
+const useLHNEstimatedListSize: UseLHNEstimatedListSize = () => undefined;
+
+export default useLHNEstimatedListSize;
diff --git a/src/hooks/useLHNEstimatedListSize/types.ts b/src/hooks/useLHNEstimatedListSize/types.ts
new file mode 100644
index 000000000000..dbc3e3b7226a
--- /dev/null
+++ b/src/hooks/useLHNEstimatedListSize/types.ts
@@ -0,0 +1,8 @@
+type UseLHNEstimatedListSize = () =>
+ | {
+ height: number;
+ width: number;
+ }
+ | undefined;
+
+export default UseLHNEstimatedListSize;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b61e383dd0f8..57fd628b43b7 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -145,6 +145,7 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
+ SettlementDateParams,
ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
@@ -2538,6 +2539,12 @@ const translations = {
cancellation: 'Cancellation policy',
cancellationUntil: 'Free cancellation until',
confirmation: 'Confirmation number',
+ cancellationPolicies: {
+ unknown: 'Unknown',
+ nonRefundable: 'Non-refundable',
+ freeCancellationUntil: 'Free cancellation until',
+ partiallyRefundable: 'Partially refundable',
+ },
},
car: 'Car',
carDetails: {
@@ -3506,6 +3513,8 @@ const translations = {
limit: 'Limit',
currentBalance: 'Current balance',
currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.',
+ balanceWillBeSettledOn: ({settlementDate}: SettlementDateParams) => `Balance will be settled on ${settlementDate}`,
+ settleBalance: 'Settle balance',
cardLimit: 'Card limit',
remainingLimit: 'Remaining limit',
requestLimitIncrease: 'Request limit increase',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 70c2b16975db..ec9fa4f6a633 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -144,6 +144,7 @@ import type {
SetTheRequestParams,
SettledAfterAddedBankAccountParams,
SettleExpensifyCardParams,
+ SettlementDateParams,
ShareParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
@@ -2562,6 +2563,12 @@ const translations = {
cancellation: 'Política de cancelación',
cancellationUntil: 'Cancelación gratuita hasta el',
confirmation: 'Número de confirmación',
+ cancellationPolicies: {
+ unknown: 'Desconocido',
+ nonRefundable: 'No reembolsable',
+ freeCancellationUntil: 'Cancelación gratuita hasta',
+ partiallyRefundable: 'Parcialmente reembolsable',
+ },
},
car: 'Auto',
carDetails: {
@@ -3547,6 +3554,8 @@ const translations = {
currentBalance: 'Saldo actual',
currentBalanceDescription:
'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.',
+ balanceWillBeSettledOn: ({settlementDate}: SettlementDateParams) => `El saldo se liquidará el ${settlementDate}.`,
+ settleBalance: 'Liquidar saldo',
cardLimit: 'Límite de la tarjeta',
remainingLimit: 'Límite restante',
requestLimitIncrease: 'Solicitar aumento de límite',
diff --git a/src/languages/params.ts b/src/languages/params.ts
index cc5132bed2a0..f3d48acc63b1 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -603,6 +603,10 @@ type FlightLayoverParams = {
layover: string;
};
+type SettlementDateParams = {
+ settlementDate: string;
+};
+
export type {
AuthenticationErrorParams,
ImportMembersSuccessfullDescriptionParams,
@@ -816,4 +820,5 @@ export type {
ChatWithAccountManagerParams,
EditDestinationSubtitleParams,
FlightLayoverParams,
+ SettlementDateParams,
};
diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
index e778f138822d..cd5beea8fa3a 100644
--- a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
@@ -26,6 +26,8 @@ type CategorizeTrackedExpenseParams = {
policyExpenseCreatedReportActionID?: string;
adminsChatReportID?: string;
adminsCreatedReportActionID?: string;
+ guidedSetupData?: string;
+ engagementChoice?: string;
};
export default CategorizeTrackedExpenseParams;
diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts
index 313ef1bd6268..baa49e58c2a9 100644
--- a/src/libs/API/parameters/CreateWorkspaceParams.ts
+++ b/src/libs/API/parameters/CreateWorkspaceParams.ts
@@ -11,6 +11,7 @@ type CreateWorkspaceParams = {
customUnitID: string;
customUnitRateID: string;
engagementChoice?: string;
+ guidedSetupData?: string;
currency: string;
file?: File;
};
diff --git a/src/libs/API/parameters/QueueExpensifyCardForBillingParams.ts b/src/libs/API/parameters/QueueExpensifyCardForBillingParams.ts
new file mode 100644
index 000000000000..80326a7837c9
--- /dev/null
+++ b/src/libs/API/parameters/QueueExpensifyCardForBillingParams.ts
@@ -0,0 +1,6 @@
+type QueueExpensifyCardForBillingParams = {
+ feedCountry: string;
+ domainAccountID: number;
+};
+
+export default QueueExpensifyCardForBillingParams;
diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
index 69979c331384..d87f78525008 100644
--- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
@@ -26,6 +26,8 @@ type ShareTrackedExpenseParams = {
policyExpenseCreatedReportActionID?: string;
adminsChatReportID?: string;
adminsCreatedReportActionID?: string;
+ engagementChoice?: string;
+ guidedSetupData?: string;
};
export default ShareTrackedExpenseParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 7b884b5f1e9d..f6c547a45511 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -364,6 +364,7 @@ export type {default as DismissProductTrainingParams} from './DismissProductTrai
export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage';
export type {default as ResetSMSDeliveryFailureStatusParams} from './ResetSMSDeliveryFailureStatusParams';
export type {default as CreatePerDiemRequestParams} from './CreatePerDiemRequestParams';
+export type {default as QueueExpensifyCardForBillingParams} from './QueueExpensifyCardForBillingParams';
export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboardingFieldsParams';
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 6dbe84f6ee64..1e2a08795562 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -406,6 +406,7 @@ const WRITE_COMMANDS = {
CONFIGURE_EXPENSIFY_CARDS_FOR_POLICY: 'ConfigureExpensifyCardsForPolicy',
CREATE_EXPENSIFY_CARD: 'CreateExpensifyCard',
CREATE_ADMIN_ISSUED_VIRTUAL_CARD: 'CreateAdminIssuedVirtualCard',
+ QUEUE_EXPENSIFY_CARD_FOR_BILLING: 'Domain_QueueExpensifyCardForBilling',
ADD_DELEGATE: 'AddDelegate',
REMOVE_DELEGATE: 'RemoveDelegate',
UPDATE_DELEGATE_ROLE: 'UpdateDelegateRole',
@@ -875,6 +876,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.CONFIGURE_EXPENSIFY_CARDS_FOR_POLICY]: Parameters.ConfigureExpensifyCardsForPolicyParams;
[WRITE_COMMANDS.CREATE_EXPENSIFY_CARD]: Omit;
[WRITE_COMMANDS.CREATE_ADMIN_ISSUED_VIRTUAL_CARD]: Omit;
+ [WRITE_COMMANDS.QUEUE_EXPENSIFY_CARD_FOR_BILLING]: Parameters.QueueExpensifyCardForBillingParams;
[WRITE_COMMANDS.ADD_DELEGATE]: Parameters.AddDelegateParams;
[WRITE_COMMANDS.UPDATE_DELEGATE_ROLE]: Parameters.UpdateDelegateRoleParams;
[WRITE_COMMANDS.REMOVE_DELEGATE]: Parameters.RemoveDelegateParams;
diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts
index 69aabf9ba499..622692b25578 100644
--- a/src/libs/CardUtils.ts
+++ b/src/libs/CardUtils.ts
@@ -98,11 +98,20 @@ function isCardHiddenFromSearch(card: Card) {
return !card?.nameValuePairs?.isVirtual && CONST.EXPENSIFY_CARD.HIDDEN_FROM_SEARCH_STATES.includes(card.state ?? 0);
}
-function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList = allCards) {
- const feedCards: CardList = {...cardList};
+function mergeCardListWithWorkspaceFeeds(workspaceFeeds: Record, cardList = allCards, shouldExcludeCardHiddenFromSearch = false) {
+ const feedCards: CardList = {};
+ Object.keys(cardList).forEach((cardKey) => {
+ const card = cardList[cardKey];
+ if (shouldExcludeCardHiddenFromSearch && isCardHiddenFromSearch(card)) {
+ return;
+ }
+
+ feedCards[cardKey] = card;
+ });
+
Object.values(workspaceFeeds ?? {}).forEach((currentCardFeed) => {
Object.values(currentCardFeed ?? {}).forEach((card) => {
- if (!isCard(card)) {
+ if (!isCard(card) || (shouldExcludeCardHiddenFromSearch && isCardHiddenFromSearch(card))) {
return;
}
feedCards[card.cardID] = card;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 435bb0d00538..e1ea9013c756 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -62,7 +62,7 @@ import {createDraftTransaction, getIOUReportActionToApproveOrPay, setMoneyReques
import {createDraftWorkspace} from './actions/Policy/Policy';
import {autoSwitchToFocusMode} from './actions/PriorityMode';
import {hasCreditBankAccount} from './actions/ReimbursementAccount/store';
-import {handleReportChanged} from './actions/Report';
+import {handleReportChanged, prepareOnboardingOnyxData} from './actions/Report';
import {isAnonymousUser as isAnonymousUserSession} from './actions/Session';
import {convertToDisplayString, getCurrencySymbol} from './CurrencyUtils';
import DateUtils from './DateUtils';
@@ -6452,6 +6452,7 @@ function buildOptimisticTaskReport(
description?: string,
policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE,
notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ shouldEscapeText = true,
): OptimisticTaskReport {
const participants: Participants = {
[ownerAccountID]: {
@@ -6466,7 +6467,7 @@ function buildOptimisticTaskReport(
return {
reportID: generateReportID(),
reportName: getParsedComment(title ?? ''),
- description: getParsedComment(description ?? ''),
+ description: getParsedComment(description ?? '', {shouldEscapeText}),
ownerAccountID,
participants,
managerID: assigneeAccountID,
@@ -9255,6 +9256,7 @@ export {
getReportMetadata,
buildOptimisticSelfDMReport,
isHiddenForCurrentUser,
+ prepareOnboardingOnyxData,
};
export type {
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 99f26978f1da..9c673288589e 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -430,7 +430,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(
* @param [file] Optional, avatar file for workspace
*/
function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency = '', file?: File) {
- createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file);
+ createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, CONST.ONBOARDING_CHOICES.MANAGE_TEAM, currency, file);
}
/**
diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts
index 8c784029e3ca..9c56e5c2021b 100644
--- a/src/libs/actions/Card.ts
+++ b/src/libs/actions/Card.ts
@@ -897,6 +897,15 @@ function updateSelectedFeed(feed: CompanyCardFeed, policyID: string | undefined)
]);
}
+function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: number) {
+ const parameters = {
+ feedCountry,
+ domainAccountID,
+ };
+
+ API.write(WRITE_COMMANDS.QUEUE_EXPENSIFY_CARD_FOR_BILLING, parameters);
+}
+
export {
requestReplacementExpensifyCard,
activatePhysicalExpensifyCard,
@@ -920,5 +929,6 @@ export {
updateSelectedFeed,
deactivateCard,
getCardDefaultName,
+ queueExpensifyCardForBilling,
};
export type {ReplacementReason};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 958839e2272c..4723009822df 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -46,6 +46,7 @@ import {
navigateToStartMoneyRequestStep,
updateIOUOwnerAndTotal,
} from '@libs/IOUUtils';
+import isFileUploadable from '@libs/isFileUploadable';
import {formatPhoneNumber} from '@libs/LocalePhoneNumber';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
@@ -148,6 +149,7 @@ import {
getUpdatedTransaction,
hasReceipt as hasReceiptTransactionUtils,
isAmountMissing,
+ isCustomUnitRateIDForP2P,
isDistanceRequest as isDistanceRequestTransactionUtils,
isExpensifyCardTransaction,
isFetchingWaypointsFromServer,
@@ -3038,7 +3040,7 @@ function getTrackExpenseInformation(
let createdWorkspaceParams: CreateWorkspaceParams | undefined;
if (isDraftReportLocal) {
- const workspaceData = buildPolicyData(undefined, policy?.makeMeAdmin, policy?.name, policy?.id, chatReport?.reportID);
+ const workspaceData = buildPolicyData(undefined, policy?.makeMeAdmin, policy?.name, policy?.id, chatReport?.reportID, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE);
createdWorkspaceParams = workspaceData.params;
optimisticData.push(...workspaceData.optimisticData);
successData.push(...workspaceData.successData);
@@ -4291,6 +4293,8 @@ function categorizeTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID,
+ engagementChoice: createdWorkspaceParams?.engagementChoice,
+ guidedSetupData: createdWorkspaceParams?.guidedSetupData,
};
API.write(WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData});
@@ -4349,6 +4353,8 @@ function shareTrackedExpense(trackedExpenseParams: TrackedExpenseParams) {
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
adminsCreatedReportActionID: createdWorkspaceParams?.adminsCreatedReportActionID,
+ engagementChoice: createdWorkspaceParams?.engagementChoice,
+ guidedSetupData: createdWorkspaceParams?.guidedSetupData,
};
API.write(WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData});
@@ -4425,7 +4431,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) {
const workspaceParams =
isPolicyExpenseChatReportUtil(chatReport) && chatReport.policyID
? {
- receipt: receipt instanceof Blob ? receipt : undefined,
+ receipt: isFileUploadable(receipt) ? receipt : undefined,
category,
tag,
taxCode,
@@ -4477,7 +4483,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) {
createdChatReportActionID,
createdIOUReportActionID,
reportPreviewReportActionID: reportPreviewAction.reportActionID,
- receipt: receipt instanceof Blob ? receipt : undefined,
+ receipt: isFileUploadable(receipt) ? receipt : undefined,
receiptState: receipt?.state,
category,
tag,
@@ -4728,6 +4734,8 @@ function trackExpense(
value: recentServerValidatedWaypoints,
});
+ const mileageRate = isCustomUnitRateIDForP2P(transaction) ? undefined : customUnitRateID;
+
switch (action) {
case CONST.IOU.ACTION.CATEGORIZE: {
if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) {
@@ -4745,7 +4753,9 @@ function trackExpense(
category,
tag,
billable,
- receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
+ receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined,
+ waypoints: sanitizedWaypoints,
+ customUnitRateID: mileageRate,
};
const policyParams: TrackedExpensePolicyParams = {
policyID: chatReport?.policyID,
@@ -4788,9 +4798,9 @@ function trackExpense(
category,
tag,
billable,
- receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
+ receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined,
waypoints: sanitizedWaypoints,
- customUnitRateID,
+ customUnitRateID: mileageRate,
};
const policyParams: TrackedExpensePolicyParams = {
policyID: chatReport?.policyID,
@@ -4829,7 +4839,7 @@ function trackExpense(
createdChatReportActionID,
createdIOUReportActionID,
reportPreviewReportActionID: reportPreviewAction?.reportActionID,
- receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
+ receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined,
receiptState: trackedReceipt?.state,
category,
tag,
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index c4752be4c974..35272f6c231c 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -78,9 +78,11 @@ import * as ReportUtils from '@libs/ReportUtils';
import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover';
import * as PaymentMethods from '@userActions/PaymentMethods';
import * as PersistedRequests from '@userActions/PersistedRequests';
+import type {OnboardingPurpose} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {
+ IntroSelected,
InvitedEmailsToAccountIDs,
PersonalDetailsList,
Policy,
@@ -1698,6 +1700,12 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol
Onyx.update(optimisticData);
}
+let introSelected: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.NVP_INTRO_SELECTED,
+ callback: (value) => (introSelected = value),
+});
+
/**
* Generates onyx data for creating a new workspace
*
@@ -1709,6 +1717,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol
* @param [engagementChoice] Purpose of using application selected by user in guided setup flow
* @param [currency] Optional, selected currency for the workspace
* @param [file] Optional, avatar file for workspace
+ * @param [shouldAddOnboardingTasks] whether to add onboarding tasks to the workspace
*/
function buildPolicyData(
policyOwnerEmail = '',
@@ -1716,9 +1725,10 @@ function buildPolicyData(
policyName = '',
policyID = generatePolicyID(),
expenseReportId?: string,
- engagementChoice?: string,
+ engagementChoice?: OnboardingPurpose,
currency = '',
file?: File,
+ shouldAddOnboardingTasks = true,
) {
const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
@@ -1991,6 +2001,21 @@ function buildPolicyData(
file: clonedFile,
};
+ if (!introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) {
+ const onboardingData = ReportUtils.prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID);
+ if (!onboardingData) {
+ return {successData, optimisticData, failureData, params};
+ }
+ const {guidedSetupData, optimisticData: taskOptimisticData, successData: taskSuccessData, failureData: taskFailureData} = onboardingData;
+
+ params.guidedSetupData = JSON.stringify(guidedSetupData);
+ params.engagementChoice = engagementChoice;
+
+ optimisticData.push(...taskOptimisticData);
+ successData.push(...taskSuccessData);
+ failureData.push(...taskFailureData);
+ }
+
return {successData, optimisticData, failureData, params};
}
@@ -2010,11 +2035,22 @@ function createWorkspace(
makeMeAdmin = false,
policyName = '',
policyID = generatePolicyID(),
- engagementChoice = '',
+ engagementChoice: OnboardingPurpose = CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
currency = '',
file?: File,
+ shouldAddOnboardingTasks = true,
): CreateWorkspaceParams {
- const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file);
+ const {optimisticData, failureData, successData, params} = buildPolicyData(
+ policyOwnerEmail,
+ makeMeAdmin,
+ policyName,
+ policyID,
+ undefined,
+ engagementChoice,
+ currency,
+ file,
+ shouldAddOnboardingTasks,
+ );
API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData});
// Publish a workspace created event if this is their first policy
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index a1d8bf4725e8..b77b8115cb59 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -932,7 +932,7 @@ function openReport(
onboardingMessage.tasks = updatedTasks;
}
- const onboardingData = prepareOnboardingOptimisticData(choice, onboardingMessage);
+ const onboardingData = prepareOnboardingOnyxData(choice, onboardingMessage);
if (onboardingData) {
optimisticData.push(...onboardingData.optimisticData, {
@@ -3616,7 +3616,7 @@ function getReportPrivateNote(reportID: string | undefined) {
API.read(READ_COMMANDS.GET_REPORT_PRIVATE_NOTE, parameters, {optimisticData, successData, failureData});
}
-function prepareOnboardingOptimisticData(
+function prepareOnboardingOnyxData(
engagementChoice: OnboardingPurpose,
data: ValueOf,
adminsChatReportID?: string,
@@ -3635,11 +3635,16 @@ function prepareOnboardingOptimisticData(
data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT];
}
- // Guides are assigned and tasks are posted in the #admins room for the MANAGE_TEAM onboarding action, except for emails that have a '+'.
- const shouldPostTasksInAdminsRoom = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !currentUserEmail?.includes('+');
+ // 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 '+'.
+ type PostTasksInAdminsRoomOnboardingChoices = 'newDotManageTeam' | 'newDotTrackWorkspace';
+ const shouldPostTasksInAdminsRoom =
+ [CONST.ONBOARDING_CHOICES.MANAGE_TEAM, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE].includes(engagementChoice as PostTasksInAdminsRoomOnboardingChoices) &&
+ !currentUserEmail?.includes('+');
const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
- const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]);
- const {reportID: targetChatReportID, policyID: targetChatPolicyID} = targetChatReport ?? {};
+ const targetChatReport = shouldPostTasksInAdminsRoom
+ ? adminsChatReport ?? {reportID: adminsChatReportID, policyID: onboardingPolicyID}
+ : getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]);
+ const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {};
if (!targetChatReportID) {
Log.warn('Missing reportID for onboarding optimistic data');
@@ -3650,7 +3655,7 @@ function prepareOnboardingOptimisticData(
const assignedGuideEmail = getPolicy(targetChatPolicyID)?.assignedGuide?.email ?? 'Setup Specialist';
const assignedGuidePersonalDetail = Object.values(allPersonalDetails ?? {}).find((personalDetail) => personalDetail?.login === assignedGuideEmail);
let assignedGuideAccountID: number;
- if (assignedGuidePersonalDetail) {
+ if (assignedGuidePersonalDetail && assignedGuidePersonalDetail.accountID) {
assignedGuideAccountID = assignedGuidePersonalDetail.accountID;
} else {
assignedGuideAccountID = generateAccountID(assignedGuideEmail);
@@ -3672,6 +3677,7 @@ function prepareOnboardingOptimisticData(
reportComment: textComment.commentText,
};
+ let createWorkspaceTaskReportID;
const tasksData = data.tasks
.filter((task) => {
if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) {
@@ -3681,6 +3687,20 @@ function prepareOnboardingOptimisticData(
if (['addAccountingIntegration', 'setupCategoriesAndTags'].includes(task.type) && !userReportedIntegration) {
return false;
}
+ type SkipViewTourOnboardingChoices = 'newDotSubmit' | 'newDotSplitChat' | 'newDotPersonalSpend' | 'newDotEmployer';
+ if (
+ task.type === 'viewTour' &&
+ [
+ CONST.ONBOARDING_CHOICES.EMPLOYER,
+ CONST.ONBOARDING_CHOICES.PERSONAL_SPEND,
+ CONST.ONBOARDING_CHOICES.SUBMIT,
+ CONST.ONBOARDING_CHOICES.CHAT_SPLIT,
+ CONST.ONBOARDING_CHOICES.MANAGE_TEAM,
+ ].includes(introSelected?.choice as SkipViewTourOnboardingChoices) &&
+ engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM
+ ) {
+ return false;
+ }
return true;
})
.map((task, index) => {
@@ -3711,6 +3731,7 @@ function prepareOnboardingOptimisticData(
taskDescription,
targetChatPolicyID,
CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ false,
);
const emailCreatingAction =
engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? allPersonalDetails?.[actorAccountID]?.login ?? CONST.EMAIL.CONCIERGE : CONST.EMAIL.CONCIERGE;
@@ -3721,6 +3742,9 @@ function prepareOnboardingOptimisticData(
const completedTaskReportAction = task.autoCompleted
? buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, 'marked as complete', actorAccountID, 2)
: null;
+ if (task.type === 'createWorkspace') {
+ createWorkspaceTaskReportID = currentTask.reportID;
+ }
return {
task,
@@ -3896,7 +3920,6 @@ function prepareOnboardingOptimisticData(
const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData];
const lastVisibleActionCreated = welcomeSignOffCommentAction.created;
-
optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -3911,12 +3934,15 @@ function prepareOnboardingOptimisticData(
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_INTRO_SELECTED,
- value: {choice: engagementChoice},
+ value: {
+ choice: engagementChoice,
+ createWorkspace: createWorkspaceTaskReportID,
+ },
},
);
- // If we post tasks in the #admins room, it means that a guide is assigned and all messages except tasks are handled by the backend
- if (!shouldPostTasksInAdminsRoom) {
+ // If we post tasks in the #admins room and introSelected?.choice does not exist, it means that a guide is assigned and all messages except tasks are handled by the backend
+ if (!shouldPostTasksInAdminsRoom || !!introSelected?.choice) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
@@ -3936,8 +3962,8 @@ function prepareOnboardingOptimisticData(
const successData: OnyxUpdate[] = [...tasksForSuccessData];
- // If we post tasks in the #admins room, it means that a guide is assigned and all messages except tasks are handled by the backend
- if (!shouldPostTasksInAdminsRoom) {
+ // If we post tasks in the #admins room and introSelected?.choice does not exist, it means that a guide is assigned and all messages except tasks are handled by the backend
+ if (!shouldPostTasksInAdminsRoom || !!introSelected?.choice) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
@@ -3977,11 +4003,14 @@ function prepareOnboardingOptimisticData(
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_INTRO_SELECTED,
- value: {choice: null},
+ value: {
+ choice: null,
+ createWorkspace: null,
+ },
},
);
- // If we post tasks in the #admins room, it means that a guide is assigned and all messages except tasks are handled by the backend
- if (!shouldPostTasksInAdminsRoom) {
+ // If we post tasks in the #admins room and introSelected?.choice does not exist, it means that a guide is assigned and all messages except tasks are handled by the backend
+ if (!shouldPostTasksInAdminsRoom || !!introSelected?.choice) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
@@ -4033,8 +4062,12 @@ function prepareOnboardingOptimisticData(
});
}
- // If we post tasks in the #admins room, it means that a guide is assigned and all messages except tasks are handled by the backend
- const guidedSetupData: GuidedSetupData = shouldPostTasksInAdminsRoom ? [] : [{type: 'message', ...textMessage}];
+ // If we post tasks in the #admins room and introSelected?.choice does not exist, it means that a guide is assigned and all messages except tasks are handled by the backend
+ const guidedSetupData: GuidedSetupData = [];
+
+ if (!shouldPostTasksInAdminsRoom || !!introSelected?.choice) {
+ guidedSetupData.push({type: 'message', ...textMessage});
+ }
type SelfDMParameters = {
reportID?: string;
@@ -4110,33 +4143,36 @@ function prepareOnboardingOptimisticData(
}
}
- optimisticData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
- value: {
- [welcomeSignOffCommentAction.reportActionID]: welcomeSignOffCommentAction as ReportAction,
- },
- });
+ guidedSetupData.push(...tasksForParameters);
- successData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
- value: {
- [welcomeSignOffCommentAction.reportActionID]: {pendingAction: null},
- },
- });
+ if (!introSelected?.choice) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: welcomeSignOffCommentAction as ReportAction,
+ },
+ });
- failureData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
- value: {
- [welcomeSignOffCommentAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
- } as ReportAction,
- },
- });
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: {pendingAction: null},
+ },
+ });
- guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage});
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
+ value: {
+ [welcomeSignOffCommentAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
+ } as ReportAction,
+ },
+ });
+ guidedSetupData.push({type: 'message', ...welcomeSignOffMessage});
+ }
return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters};
}
@@ -4153,7 +4189,7 @@ function completeOnboarding(
userReportedIntegration?: OnboardingAccounting,
wasInvited?: boolean,
) {
- const onboardingData = prepareOnboardingOptimisticData(engagementChoice, data, adminsChatReportID, onboardingPolicyID, userReportedIntegration, wasInvited);
+ const onboardingData = prepareOnboardingOnyxData(engagementChoice, data, adminsChatReportID, onboardingPolicyID, userReportedIntegration, wasInvited);
if (!onboardingData) {
return;
}
@@ -4705,4 +4741,5 @@ export {
getConciergeReportID,
setDeleteTransactionNavigateBackUrl,
clearDeleteTransactionNavigateBackUrl,
+ prepareOnboardingOnyxData,
};
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 9764296c3a9a..c8c9ae12773d 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -30,7 +30,6 @@ 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 * as Pusher from '@libs/Pusher/pusher';
@@ -972,7 +971,6 @@ function checkforMissingPongEvents() {
// If the oldest timestamp is older than 2 * PING_INTERVAL_LENGTH_IN_SECONDS, then set the network status to offline
if (ageOfEventInMS > NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS * 1000) {
Log.info(`[Pusher PINGPONG] The server has not replied to the PING event ${eventID} in ${ageOfEventInMS} ms so going offline`);
- NetworkConnection.setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event');
// When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh
lastTimestamp = Date.now();
diff --git a/src/libs/isFileUploadable/index.native.ts b/src/libs/isFileUploadable/index.native.ts
new file mode 100644
index 000000000000..602d2c319a74
--- /dev/null
+++ b/src/libs/isFileUploadable/index.native.ts
@@ -0,0 +1,10 @@
+import type {FileObject} from '@components/AttachmentModal';
+
+function isFileUploadable(file: FileObject | undefined): boolean {
+ // Native platforms only require the object to include the `uri` property.
+ // Optionally, it can also have a `name` and `type` properties.
+ // On other platforms, the file must be an instance of `Blob` or one of its subclasses.
+ return !!file && 'uri' in file && !!file.uri && typeof file.uri === 'string';
+}
+
+export default isFileUploadable;
diff --git a/src/libs/isFileUploadable/index.ts b/src/libs/isFileUploadable/index.ts
new file mode 100644
index 000000000000..a2f985ed109d
--- /dev/null
+++ b/src/libs/isFileUploadable/index.ts
@@ -0,0 +1,7 @@
+import type {FileObject} from '@components/AttachmentModal';
+
+function isFileUploadable(file: FileObject | undefined): boolean {
+ return file instanceof Blob;
+}
+
+export default isFileUploadable;
diff --git a/src/libs/validateFormDataParameter.ts b/src/libs/validateFormDataParameter.ts
index 02b05133e1dc..dd46c97a7941 100644
--- a/src/libs/validateFormDataParameter.ts
+++ b/src/libs/validateFormDataParameter.ts
@@ -1,8 +1,4 @@
-import CONST from '@src/CONST';
-import getPlatform from './getPlatform';
-
-const platform = getPlatform();
-const isNativePlatform = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS;
+import isFileUploadable from './isFileUploadable';
/**
* Ensures no value of type `object` other than null, Blob, its subclasses, or {uri: string} (native platforms only) is passed to XMLHttpRequest.
@@ -19,10 +15,7 @@ function validateFormDataParameter(command: string, key: string, value: unknown)
return value.every((element) => isValid(element, false));
}
if (isTopLevel) {
- // Native platforms only require the value to include the `uri` property.
- // Optionally, it can also have a `name` and `type` props.
- // On other platforms, the value must be an instance of `Blob`.
- return isNativePlatform ? 'uri' in value && !!value.uri : value instanceof Blob;
+ return isFileUploadable(value);
}
return false;
};
diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
index 3f52722fc30e..93b512efdab1 100644
--- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
+++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx
@@ -68,7 +68,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount
return;
}
- const {adminsChatReportID, policyID} = createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
+ const {adminsChatReportID, policyID} = createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false);
setOnboardingAdminsChatReportID(adminsChatReportID);
setOnboardingPolicyID(policyID);
}, [isVsb, paidGroupPolicy, allPolicies, allPoliciesResult]);
diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
index 12722e87f05a..f09f1ada0d34 100644
--- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
+++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
@@ -13,10 +13,10 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as Policy from '@userActions/Policy/Policy';
-import * as Report from '@userActions/Report';
-import * as Welcome from '@userActions/Welcome';
+import {isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {createWorkspace, generatePolicyID} from '@userActions/Policy/Policy';
+import {completeOnboarding} from '@userActions/Report';
+import {setOnboardingAdminsChatReportID, setOnboardingCompanySize, setOnboardingPolicyID} from '@userActions/Welcome';
import CONST from '@src/CONST';
import type {OnboardingCompanySize} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -35,7 +35,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE
const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
- const paidGroupPolicy = Object.values(allPolicies ?? {}).find(PolicyUtils.isPaidGroupPolicy);
+ const paidGroupPolicy = Object.values(allPolicies ?? {}).find(isPaidGroupPolicy);
const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
const [selectedCompanySize, setSelectedCompanySize] = useState(onboardingCompanySize);
@@ -69,19 +69,19 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE
setError(translate('onboarding.errorSelection'));
return;
}
- Welcome.setOnboardingCompanySize(selectedCompanySize);
+ setOnboardingCompanySize(selectedCompanySize);
const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy;
- // We need `adminsChatReportID` for `Report.completeOnboarding`, but at the same time, we don't want to call `Policy.createWorkspace` more than once.
+ // We need `adminsChatReportID` for `completeOnboarding`, but at the same time, we don't want to call `createWorkspace` more than once.
// If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`.
const {adminsChatReportID, policyID} = shouldCreateWorkspace
- ? Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM)
+ ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, '', undefined, false)
: {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID};
if (shouldCreateWorkspace) {
- Welcome.setOnboardingAdminsChatReportID(adminsChatReportID);
- Welcome.setOnboardingPolicyID(policyID);
+ setOnboardingAdminsChatReportID(adminsChatReportID);
+ setOnboardingPolicyID(policyID);
}
// For MICRO companies (1-10 employees), we want to remain on NewDot.
@@ -93,7 +93,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE
// For other company sizes we want to complete onboarding here.
// At this point `onboardingPurposeSelected` should always exist as we set it in `BaseOnboardingPurpose`.
if (onboardingPurposeSelected) {
- Report.completeOnboarding(
+ completeOnboarding(
onboardingPurposeSelected,
CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected],
undefined,
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 005c5c96837b..026f5ea756c4 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -376,7 +376,7 @@ function AdvancedSearchFilters() {
const policyID = searchAdvancedFilters.policyID;
const [userCardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
const [workspaceCardFeeds = {}] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST);
- const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]);
+ const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList, true), [userCardList, workspaceCardFeeds]);
const taxRates = getAllTaxRates();
const personalDetails = usePersonalDetails();
diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx
index 747dc3ceca70..4df54faf1fc1 100644
--- a/src/pages/Travel/HotelTripDetails.tsx
+++ b/src/pages/Travel/HotelTripDetails.tsx
@@ -1,3 +1,4 @@
+import {Str} from 'expensify-common';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -20,17 +21,24 @@ function HotelTripDetails({reservation, personalDetails}: HotelTripDetailsProps)
const styles = useThemeStyles();
const {translate} = useLocalize();
+ const cancellationMapping: Record = {
+ [CONST.CANCELLATION_POLICY.UNKNOWN]: translate('travel.hotelDetails.cancellationPolicies.unknown'),
+ [CONST.CANCELLATION_POLICY.NON_REFUNDABLE]: translate('travel.hotelDetails.cancellationPolicies.nonRefundable'),
+ [CONST.CANCELLATION_POLICY.FREE_CANCELLATION_UNTIL]: translate('travel.hotelDetails.cancellationPolicies.freeCancellationUntil'),
+ [CONST.CANCELLATION_POLICY.PARTIALLY_REFUNDABLE]: translate('travel.hotelDetails.cancellationPolicies.partiallyRefundable'),
+ };
+
const checkInDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.start.date));
const checkOutDate = DateUtils.getFormattedTransportDateAndHour(new Date(reservation.end.date));
const cancellationText = reservation.cancellationDeadline
? `${translate('travel.hotelDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(reservation.cancellationDeadline)).date}`
- : reservation.cancellationPolicy;
+ : cancellationMapping[reservation.cancellationPolicy ?? CONST.CANCELLATION_POLICY.UNKNOWN];
const displayName = personalDetails?.displayName ?? reservation.travelerPersonalInfo?.name;
return (
<>
- {reservation.start.longName}
+ {Str.recapitalize(reservation.start.longName ?? '')}
void;
+
+ /* Render the FAB as an emoji */
+ isEmoji?: boolean;
+
+ /* Emoji content to render when isEmoji */
+ children?: ReactNode;
};
type FloatingActionButtonAndPopoverRef = {
@@ -166,7 +173,7 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => {
* Responsible for rendering the {@link PopoverMenu}, and the accompanying
* FAB that can open or close the menu.
*/
-function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) {
+function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isEmoji, children}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
@@ -462,102 +469,130 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
const canModifyTask = canModifyTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID);
const canActionTask = canActionTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID);
- return (
-
- interceptAnonymousUser(startNewChat),
- },
- ...(canSendInvoice
- ? [
- {
- icon: Expensicons.InvoiceGeneric,
- text: translate('workspace.invoices.sendInvoice'),
- shouldCallAfterModalHide: shouldRedirectToExpensifyClassic,
- onSelected: () =>
- interceptAnonymousUser(() => {
- if (shouldRedirectToExpensifyClassic) {
- setModalVisible(true);
- return;
- }
-
- startMoneyRequest(
- CONST.IOU.TYPE.INVOICE,
- // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
- // for all of the routes in the creation flow.
- generateReportID(),
- );
- }),
- },
- ]
- : []),
- ...(canUseSpotnanaTravel
- ? [
- {
- icon: Expensicons.Suitcase,
- text: translate('travel.bookTravel'),
- onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)),
- },
- ]
- : []),
- ...(!hasSeenTour
- ? [
- {
- icon: Expensicons.Binoculars,
- iconStyles: styles.popoverIconCircle,
- iconFill: theme.icon,
- text: translate('tour.takeATwoMinuteTour'),
- description: translate('tour.exploreExpensify'),
- onSelected: () => {
- openExternalLink(navatticURL);
- setSelfTourViewed(isAnonymousUser());
- if (viewTourTaskReport && canModifyTask && canActionTask) {
- completeTask(viewTourTaskReport);
+ const popoverMenu = (
+ interceptAnonymousUser(startNewChat),
+ },
+ ...(canSendInvoice
+ ? [
+ {
+ icon: Expensicons.InvoiceGeneric,
+ text: translate('workspace.invoices.sendInvoice'),
+ shouldCallAfterModalHide: shouldRedirectToExpensifyClassic,
+ onSelected: () =>
+ interceptAnonymousUser(() => {
+ if (shouldRedirectToExpensifyClassic) {
+ setModalVisible(true);
+ return;
}
- },
- },
- ]
- : []),
- ...(!isLoading && shouldShowNewWorkspaceButton
- ? [
- {
- displayInDefaultIconColor: true,
- contentFit: 'contain' as ImageContentFit,
- icon: Expensicons.NewWorkspace,
- iconWidth: variables.w46,
- iconHeight: variables.h40,
- text: translate('workspace.new.newWorkspace'),
- description: translate('workspace.new.getTheExpensifyCardAndMore'),
- onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))),
+
+ startMoneyRequest(
+ CONST.IOU.TYPE.INVOICE,
+ // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
+ // for all of the routes in the creation flow.
+ generateReportID(),
+ );
+ }),
+ },
+ ]
+ : []),
+ ...(canUseSpotnanaTravel
+ ? [
+ {
+ icon: Expensicons.Suitcase,
+ text: translate('travel.bookTravel'),
+ onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)),
+ },
+ ]
+ : []),
+ ...(!hasSeenTour
+ ? [
+ {
+ icon: Expensicons.Binoculars,
+ iconStyles: styles.popoverIconCircle,
+ iconFill: theme.icon,
+ text: translate('tour.takeATwoMinuteTour'),
+ description: translate('tour.exploreExpensify'),
+ onSelected: () => {
+ openExternalLink(navatticURL);
+ setSelfTourViewed(isAnonymousUser());
+ if (viewTourTaskReport && canModifyTask && canActionTask) {
+ completeTask(viewTourTaskReport);
+ }
},
- ]
- : []),
- ...quickActionMenuItems,
- ]}
- withoutOverlay
- anchorRef={fabRef}
- />
- {
- setModalVisible(false);
- openOldDotLink(CONST.OLDDOT_URLS.INBOX);
- }}
- onCancel={() => setModalVisible(false)}
- title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')}
- confirmText={translate('exitSurvey.goToExpensifyClassic')}
- cancelText={translate('common.cancel')}
- />
+ },
+ ]
+ : []),
+ ...(!isLoading && shouldShowNewWorkspaceButton
+ ? [
+ {
+ displayInDefaultIconColor: true,
+ contentFit: 'contain' as ImageContentFit,
+ icon: Expensicons.NewWorkspace,
+ iconWidth: variables.w46,
+ iconHeight: variables.h40,
+ text: translate('workspace.new.newWorkspace'),
+ description: translate('workspace.new.getTheExpensifyCardAndMore'),
+ onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))),
+ },
+ ]
+ : []),
+ ...quickActionMenuItems,
+ ]}
+ withoutOverlay
+ anchorRef={fabRef}
+ />
+ );
+
+ const confirmModal = (
+ {
+ setModalVisible(false);
+ openOldDotLink(CONST.OLDDOT_URLS.INBOX);
+ }}
+ onCancel={() => setModalVisible(false)}
+ title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')}
+ confirmText={translate('exitSurvey.goToExpensifyClassic')}
+ cancelText={translate('common.cancel')}
+ />
+ );
+
+ if (isEmoji) {
+ return (
+ <>
+
+ {popoverMenu}
+ {confirmModal}
+
+
+ {children}
+
+ >
+ );
+ }
+
+ return (
+
+ {popoverMenu}
+ {confirmModal}
{
- IOU.setMoneyRequestParticipants(transactionID, [
+ setMoneyRequestParticipants(transactionID, [
{
selected: true,
accountID: 0,
isPolicyExpenseChat: true,
- reportID: policyDataRef.current?.expenseChatReportID ?? '-1',
+ reportID: policyDataRef.current?.expenseChatReportID,
policyID: policyDataRef.current?.policyID,
searchText: policyDataRef.current?.policyName,
},
]);
Navigation.goBack();
- Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, policyDataRef.current?.expenseChatReportID ?? '-1'));
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, policyDataRef.current?.expenseChatReportID));
}}
policyName=""
isCategorizing
@@ -67,7 +67,7 @@ function IOURequestStepUpgrade({
{
- const policyData = Policy.createWorkspace();
+ const policyData = Policy.createWorkspace('', false, '', undefined, CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE);
setIsUpgraded(true);
policyDataRef.current = policyData;
}}
diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx
index a8d8e2dc419f..a988c947b3eb 100644
--- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx
+++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx
@@ -19,7 +19,7 @@ import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import {clearCardListErrors, clearReportVirtualCardFraudForm, reportVirtualExpensifyCardFraud} from '@userActions/Card';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type SCREENS from '@src/SCREENS';
+import SCREENS from '@src/SCREENS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
type ReportVirtualCardFraudPageProps = PlatformStackScreenProps;
@@ -62,7 +62,8 @@ function ReportVirtualCardFraudPage({
}
if (latestIssuedVirtualCardID) {
- Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD_CONFIRMATION.getRoute(latestIssuedVirtualCardID));
+ Navigation.removeScreenFromNavigationState(SCREENS.SETTINGS.WALLET.DOMAIN_CARD);
+ Navigation.closeAndNavigate(ROUTES.SETTINGS_REPORT_FRAUD_CONFIRMATION.getRoute(latestIssuedVirtualCardID));
setIsValidateCodeActionModalVisible(false);
}
}, [formData?.isLoading, latestIssuedVirtualCardID, prevIsLoading, virtualCard?.errors]);
diff --git a/src/pages/signin/SignInPageLayout/index.tsx b/src/pages/signin/SignInPageLayout/index.tsx
index 3517edf9b847..d03d7459ac7f 100644
--- a/src/pages/signin/SignInPageLayout/index.tsx
+++ b/src/pages/signin/SignInPageLayout/index.tsx
@@ -6,15 +6,13 @@ import {View} from 'react-native';
import SignInGradient from '@assets/images/home-fade-gradient.svg';
import ImageSVG from '@components/ImageSVG';
import ScrollView from '@components/ScrollView';
-import useLocalize from '@hooks/useLocalize';
-import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import * as Browser from '@libs/Browser';
+import {isMobileSafari} from '@libs/Browser';
import DomUtils from '@libs/DomUtils';
import getPlatform from '@libs/getPlatform';
// eslint-disable-next-line no-restricted-imports
@@ -44,10 +42,8 @@ function SignInPageLayout(
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const {preferredLocale} = useLocalize();
const {top: topInsets, bottom: bottomInsets} = useSafeAreaInsets();
const scrollViewRef = useRef(null);
- const prevPreferredLocale = usePrevious(preferredLocale);
const {windowHeight} = useWindowDimensions();
const {shouldUseNarrowLayout, isMediumScreenWidth, isLargeScreenWidth} = useResponsiveLayout();
@@ -73,14 +69,6 @@ function SignInPageLayout(
scrollPageToTop,
}));
- useEffect(() => {
- if (prevPreferredLocale !== preferredLocale) {
- return;
- }
-
- scrollPageToTop();
- }, [welcomeHeader, welcomeText, prevPreferredLocale, preferredLocale]);
-
const scrollViewStyles = useMemo(() => scrollViewContentContainerStyles(styles), [styles]);
const backgroundImageHeight = Math.max(variables.signInContentMinHeight, containerHeight);
@@ -174,7 +162,7 @@ function SignInPageLayout(
style={[
styles.flex1,
styles.flexColumn,
- Browser.isMobileSafari() ? styles.overflowHidden : {},
+ isMobileSafari() ? styles.overflowHidden : {},
StyleUtils.getMinimumHeight(backgroundImageHeight),
StyleUtils.getSignInBgStyles(theme),
]}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
index e9aac5685dc1..bac9cfd387dd 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx
@@ -1,11 +1,13 @@
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
+import FormHelpMessage from '@components/FormHelpMessage';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {getLatestErrorMessage} from '@libs/ErrorUtils';
+import {getWorkspaceAccountID} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import WorkspaceCardsListLabel from './WorkspaceCardsListLabel';
@@ -21,15 +23,55 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID);
+ const workspaceAccountID = getWorkspaceAccountID(policyID);
const isLessThanMediumScreen = isMediumScreenWidth || isSmallScreenWidth;
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
+ const [cardManualBilling] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING}${workspaceAccountID}`);
- return (
-
-
-
+ const errorMessage = getLatestErrorMessage(cardSettings) ?? '';
+
+ const shouldShowSettlementButtonOrDate = !!cardSettings?.isMonthlySettlementAllowed || cardManualBilling;
+
+ const getLabelsLayout = () => {
+ if (!isLessThanMediumScreen) {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+ return shouldShowSettlementButtonOrDate ? (
+ <>
+
+
+
+
+
+ >
+ ) : (
+ <>
+
-
+ >
+ );
+ };
-
+ return (
+
+ {getLabelsLayout()}
+ {!!errorMessage && (
+
+
+
+ )}
+
policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
+ const [cardManualBilling] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_MANUAL_BILLING}${workspaceAccountID}`);
const paymentBankAccountID = cardSettings?.paymentBankAccountID;
+ const isLessThanMediumScreen = isMediumScreenWidth || shouldUseNarrowLayout;
+
const isConnectedWithPlaid = useMemo(() => {
- const bankAccountData = bankAccountList?.[paymentBankAccountID ?? 0]?.accountData;
+ const bankAccountData = bankAccountList?.[paymentBankAccountID ?? CONST.DEFAULT_NUMBER_ID]?.accountData;
// TODO: remove the extra check when plaidAccountID storing is aligned in https://github.com/Expensify/App/issues/47944
// Right after adding a bank account plaidAccountID is stored inside the accountData and not in the additionalData
@@ -80,34 +86,57 @@ function WorkspaceCardsListLabel({type, value, style}: WorkspaceCardsListLabelPr
}, [isVisible, windowWidth]);
const requestLimitIncrease = () => {
- Policy.requestExpensifyCardLimitIncrease(cardSettings?.paymentBankAccountID);
+ requestExpensifyCardLimitIncrease(cardSettings?.paymentBankAccountID);
setVisible(false);
- Report.navigateToConciergeChat();
+ navigateToConciergeChat();
+ };
+
+ const isCurrentBalanceType = type === CONST.WORKSPACE_CARDS_LIST_LABEL_TYPE.CURRENT_BALANCE;
+ const isSettleBalanceButtonDisplayed = !!cardSettings?.isMonthlySettlementAllowed && !cardManualBilling && isCurrentBalanceType;
+ const isSettleDateTextDisplayed = !!cardManualBilling && isCurrentBalanceType;
+
+ const settlementDate = isSettleDateTextDisplayed ? format(addDays(new Date(), 1), CONST.DATE.FNS_FORMAT_STRING) : '';
+
+ const handleSettleBalanceButtonClick = () => {
+ queueExpensifyCardForBilling(CONST.COUNTRY.US, workspaceAccountID);
};
return (
-
- {translate(`workspace.expensifyCard.${type}`)}
- setVisible(true)}
+
+
-
-
+ {translate(`workspace.expensifyCard.${type}`)}
+ setVisible(true)}
+ >
+
+
+
+
+ {convertToDisplayString(value, policyCurrency)}
+ {isSettleBalanceButtonDisplayed && (
+
+
+
+ )}
+
-
- {CurrencyUtils.convertToDisplayString(value, policyCurrency)}
-
+ {isSettleDateTextDisplayed && {translate('workspace.expensifyCard.balanceWillBeSettledOn', {settlementDate})}}
setVisible(false)}
isVisible={isVisible}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 85e0f45c5466..a9e1caa5c197 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -15,6 +15,7 @@ import type {ValueOf} from 'type-fest';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import {ACTIVE_LABEL_SCALE} from '@components/TextInput/styleConst';
import {getBrowser, isMobile, isMobileSafari, isSafari} from '@libs/Browser';
+import getPlatform from '@libs/getPlatform';
import CONST from '@src/CONST';
import {defaultTheme} from './theme';
import colors from './theme/colors';
@@ -1673,6 +1674,13 @@ const styles = (theme: ThemeColors) =>
justifyContent: 'center',
},
+ customEmoji: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ verticalAlign: 'bottom',
+ ...(getPlatform() === CONST.PLATFORM.IOS || getPlatform() === CONST.PLATFORM.ANDROID ? {marginBottom: -variables.iconSizeNormal / 4} : {}),
+ },
+
sidebarFooterUsername: {
color: theme.heading,
fontSize: variables.fontSizeLabel,
diff --git a/src/types/onyx/IntroSelected.ts b/src/types/onyx/IntroSelected.ts
index d8077a4a8a4a..aec09130d9cb 100644
--- a/src/types/onyx/IntroSelected.ts
+++ b/src/types/onyx/IntroSelected.ts
@@ -14,6 +14,9 @@ type IntroSelected = {
/** Task reportID for 'viewTour' type */
viewTour?: string;
+
+ /** Task reportID for 'createWorkspace' type */
+ createWorkspace?: string;
};
export default IntroSelected;
diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts
index f5a2b9ac8505..07820ce1cd30 100644
--- a/tests/actions/PolicyTest.ts
+++ b/tests/actions/PolicyTest.ts
@@ -93,8 +93,10 @@ describe('actions/Policy', () => {
});
});
- // Two reports should be created: #admins and expense report
- const workspaceReports = Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID);
+ // These reports should be created: #admins and expense report + task reports of manage team (default) intent
+ const workspaceReports = Object.values(allReports ?? {})
+ .filter((report) => report?.policyID === policyID)
+ .filter((report) => report?.type !== 'task');
expect(workspaceReports.length).toBe(2);
workspaceReports.forEach((report) => {
expect(report?.pendingFields?.addWorkspaceRoom).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
@@ -129,14 +131,35 @@ describe('actions/Policy', () => {
let adminReportActions: ReportAction[] = Object.values(reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminReportID}`] ?? {});
let expenseReportActions: ReportAction[] = Object.values(reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReportID}`] ?? {});
let workspaceReportActions: ReportAction[] = adminReportActions.concat(expenseReportActions);
- [adminReportActions, expenseReportActions].forEach((actions) => {
- expect(actions.length).toBe(1);
- });
- [...adminReportActions, ...expenseReportActions].forEach((reportAction) => {
+ expect(expenseReportActions.length).toBe(1);
+ [...expenseReportActions].forEach((reportAction) => {
expect(reportAction.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.CREATED);
expect(reportAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
expect(reportAction.actorAccountID).toBe(ESH_ACCOUNT_ID);
});
+ // Created report action and and default MANAGE_TEAM intent tasks (7) assigned to user by guide, signingoff messages (1)
+ expect(adminReportActions.length).toBe(9);
+ let createdTaskReportActions = 0;
+ let signingOffMessage = 0;
+ let taskReportActions = 0;
+ adminReportActions.forEach((reportAction) => {
+ if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
+ createdTaskReportActions++;
+ expect(reportAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
+ expect(reportAction.actorAccountID).toBe(ESH_ACCOUNT_ID);
+ return;
+ }
+ if (reportAction.childType === CONST.REPORT.TYPE.TASK) {
+ taskReportActions++;
+ expect(reportAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
+ // we dont check actorAccountID as it will be a random account id for the guide
+ return;
+ }
+ signingOffMessage++;
+ });
+ expect(createdTaskReportActions).toBe(1);
+ expect(signingOffMessage).toBe(1);
+ expect(taskReportActions).toBe(7);
// Check for success data
(fetch as MockFetch)?.resume?.();
diff --git a/tests/perf-test/SidebarLinks.perf-test.tsx b/tests/perf-test/SidebarLinks.perf-test.tsx
index 7d1e4687eb21..5b24bc4c8bd1 100644
--- a/tests/perf-test/SidebarLinks.perf-test.tsx
+++ b/tests/perf-test/SidebarLinks.perf-test.tsx
@@ -28,6 +28,7 @@ jest.mock('../../src/libs/Navigation/navigationRef', () => ({
jest.mock('@components/Icon/Expensicons');
jest.mock('@react-navigation/native');
+jest.mock('@src/hooks/useLHNEstimatedListSize/index.native.ts');
const getMockedReportsMap = (length = 100) => {
const mockReports = Object.fromEntries(