diff --git a/assets/images/scan.svg b/assets/images/scan.svg
new file mode 100644
index 0000000000000..629dc3823a12f
--- /dev/null
+++ b/assets/images/scan.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/CONST.ts b/src/CONST.ts
index 0b10e57673286..c1f310519a712 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3131,6 +3131,13 @@ const CONST = {
REPORT: 'REPORT',
},
+ INTRO_CHOICES: {
+ TRACK: 'newDotTrack',
+ SUBMIT: 'newDotSubmit',
+ MANAGE_TEAM: 'newDotManageTeam',
+ CHAT_SPLIT: 'newDotSplitChat',
+ },
+
MINI_CONTEXT_MENU_MAX_ITEMS: 4,
} as const;
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 40a43d8195de7..8a2ce5a4b63d1 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -110,6 +110,12 @@ const ONYXKEYS = {
/** This NVP holds to most recent waypoints that a person has used when creating a distance request */
NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints',
+ /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */
+ NVP_HAS_DISMISSED_IDLE_PANEL: 'hasDismissedIdlePanel',
+
+ /** This NVP contains the choice that the user made on the engagement modal */
+ NVP_INTRO_SELECTED: 'introSelected',
+
/** Does this user have push notifications enabled for this device? */
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',
@@ -393,6 +399,8 @@ type OnyxValues = {
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
+ [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean;
+ [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
[ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index bd8d535e540fe..346ff19987efe 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -458,7 +458,7 @@ function AttachmentModal(props) {
shouldShowThreeDotsButton={shouldShowThreeDotsButton}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)}
threeDotsMenuItems={threeDotsMenuItems}
- shouldOverlay
+ shouldOverlayDots
/>
{!_.isEmpty(props.report) && !props.isReceiptAttachment ? (
diff --git a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js
index 109e60adf6725..2f7ac48b558b1 100644
--- a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js
+++ b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js
@@ -96,6 +96,9 @@ const propTypes = {
/** Whether we should navigate to report page when the route have a topMostReport */
shouldNavigateToTopMostReport: PropTypes.bool,
+
+ /** Whether we should overlay the 3 dots menu */
+ shouldOverlayDots: PropTypes.bool,
};
export default propTypes;
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 6cbfde0645deb..a0f24b06db7fb 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Keyboard, View} from 'react-native';
+import {Keyboard, StyleSheet, View} from 'react-native';
import AvatarWithDisplayName from '@components/AvatarWithDisplayName';
import Header from '@components/Header';
import Icon from '@components/Icon';
@@ -52,6 +52,7 @@ function HeaderWithBackButton({
threeDotsMenuItems = [],
shouldEnableDetailPageNavigation = false,
children = null,
+ shouldOverlayDots = false,
shouldOverlay = false,
singleExecution = (func) => func,
shouldNavigateToTopMostReport = false,
@@ -69,7 +70,7 @@ function HeaderWithBackButton({
// Hover on some part of close icons will not work on Electron if dragArea is true
// https://github.com/Expensify/App/issues/29598
dataSet={{dragArea: false}}
- style={[styles.headerBar, shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2]}
+ style={[styles.headerBar, shouldShowBorderBottom && styles.borderBottom, shouldShowBackButton && styles.pl2, shouldOverlay && StyleSheet.absoluteFillObject]}
>
{shouldShowBackButton && (
@@ -163,7 +164,7 @@ function HeaderWithBackButton({
menuItems={threeDotsMenuItems}
onIconPress={onThreeDotsButtonPress}
anchorPosition={threeDotsAnchorPosition}
- shouldOverlay={shouldOverlay}
+ shouldOverlay={shouldOverlayDots}
/>
)}
{shouldShowCloseButton && (
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 55cc9e7087713..832351b2b70e7 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -108,6 +108,9 @@ type HeaderWithBackButtonProps = Partial & {
/** Whether we should enable detail page navigation */
shouldEnableDetailPageNavigation?: boolean;
+
+ /** Whether we should overlay the 3 dots menu */
+ shouldOverlayDots?: boolean;
};
export type {ThreeDotsMenuItem};
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 797e6f34fc751..364fb03a2055b 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -107,6 +107,7 @@ import ReceiptSearch from '@assets/images/receipt-search.svg';
import Receipt from '@assets/images/receipt.svg';
import Rotate from '@assets/images/rotate-image.svg';
import RotateLeft from '@assets/images/rotate-left.svg';
+import Scan from '@assets/images/scan.svg';
import Send from '@assets/images/send.svg';
import Shield from '@assets/images/shield.svg';
import AppleLogo from '@assets/images/signIn/apple-logo.svg';
@@ -243,6 +244,7 @@ export {
ReceiptSearch,
Rotate,
RotateLeft,
+ Scan,
Send,
Shield,
Sync,
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 6e5b4eddae9e9..a250e21c00212 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -9,6 +9,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import ComposerFocusManager from '@libs/ComposerFocusManager';
+import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';
import useNativeDriver from '@libs/useNativeDriver';
import variables from '@styles/variables';
import * as Modal from '@userActions/Modal';
@@ -38,6 +39,7 @@ function BaseModal(
onLayout,
avoidKeyboard = false,
children,
+ shouldUseCustomBackdrop = false,
}: BaseModalProps,
ref: React.ForwardedRef,
) {
@@ -185,7 +187,7 @@ function BaseModal(
swipeDirection={swipeDirection}
isVisible={isVisible}
backdropColor={theme.overlay}
- backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
+ backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity}
backdropTransitionOutTiming={0}
hasBackdrop={fullscreen}
coverScreen={fullscreen}
@@ -201,6 +203,7 @@ function BaseModal(
statusBarTranslucent={statusBarTranslucent}
onLayout={onLayout}
avoidKeyboard={avoidKeyboard}
+ customBackdrop={shouldUseCustomBackdrop ? : undefined}
>
& {
* See: https://github.com/react-native-modal/react-native-modal/pull/116
* */
hideModalContentWhileAnimating?: boolean;
+
+ /** Should we use a custom backdrop for the modal? (This prevents focus issues on desktop) */
+ shouldUseCustomBackdrop?: boolean;
};
export default BaseModalProps;
diff --git a/src/components/PurposeForUsingExpensifyModal.tsx b/src/components/PurposeForUsingExpensifyModal.tsx
new file mode 100644
index 0000000000000..a8cab171ffca9
--- /dev/null
+++ b/src/components/PurposeForUsingExpensifyModal.tsx
@@ -0,0 +1,178 @@
+import {useNavigation} from '@react-navigation/native';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {ScrollView, View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as Report from '@userActions/Report';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import * as Expensicons from './Icon/Expensicons';
+import Lottie from './Lottie';
+import LottieAnimations from './LottieAnimations';
+import type {MenuItemProps} from './MenuItem';
+import MenuItemList from './MenuItemList';
+import Modal from './Modal';
+import Text from './Text';
+
+// This is not translated because it is a message coming from concierge, which only supports english
+const messageCopy = {
+ [CONST.INTRO_CHOICES.TRACK]:
+ 'Great! To track your expenses, I suggest you create a workspace to keep everything contained:\n' +
+ '\n' +
+ '1. Press your avatar icon\n' +
+ '2. Choose Workspaces\n' +
+ '3. Choose New Workspace\n' +
+ '4. Name your workspace something meaningful (eg, "My Business Expenses")\n' +
+ '\n' +
+ 'Once you have your workspace set up, you can add expenses to it as follows:\n' +
+ '\n' +
+ '1. Choose My Business Expenses (or whatever you named it) in the list of chat rooms\n' +
+ '2. Choose the + button in the chat compose window\n' +
+ '3. Choose Request money\n' +
+ "4. Choose what kind of expense you'd like to log, whether a manual expense, scanned receipt, or tracked distance.\n" +
+ '\n' +
+ "That'll be stored in your My Business Expenses room for your later access. Thanks for asking, and let me know how it goes!",
+ [CONST.INTRO_CHOICES.SUBMIT]:
+ 'Hi there, to submit expenses for reimbursement, please:\n' +
+ '\n' +
+ '1. Press the big green + button\n' +
+ '2. Choose Request money\n' +
+ '3. Indicate how much to request, either manually, by scanning a receipt, or by tracking distance\n' +
+ '4. Enter the email address or phone number of your boss\n' +
+ '\n' +
+ "And we'll take it from there to get you paid back. Please give it a shot and let me know how it goes!",
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]:
+ "Great! To manage your team's expenses, create a workspace to keep everything contained:\n" +
+ '\n' +
+ '1. Press your avatar icon\n' +
+ '2. Choose Workspaces\n' +
+ '3. Choose New Workspace\n' +
+ '4. Name your workspace something meaningful (eg, "Galaxy Food Inc.")\n' +
+ '\n' +
+ 'Once you have your workspace set up, you can invite your team to it via the Members pane and connect a business bank account to reimburse them!',
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]:
+ 'Hi there, to split an expense such as with a friend, please:\n' +
+ '\n' +
+ 'Press the big green + button\n' +
+ 'Choose *Request money*\n' +
+ 'Indicate how much was spent, either manually, by scanning a receipt, or by tracking distance\n' +
+ 'Enter the email address or phone number of your friend\n' +
+ 'Press *Split* next to their name\n' +
+ 'Repeat as many times as you like for each of your friends\n' +
+ 'Press *Add to split* when done adding friends\n' +
+ 'Press Split to split the bill\n' +
+ '\n' +
+ "This will send a money request to each of your friends for however much they owe you, and we'll take care of getting you paid back. Thanks for asking, and let me know how it goes!",
+};
+
+const menuIcons = {
+ [CONST.INTRO_CHOICES.TRACK]: Expensicons.ReceiptSearch,
+ [CONST.INTRO_CHOICES.SUBMIT]: Expensicons.Scan,
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: Expensicons.MoneyBag,
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: Expensicons.Briefcase,
+};
+
+function PurposeForUsingExpensifyModal() {
+ const {translate} = useLocalize();
+ const StyleUtils = useStyleUtils();
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
+ const navigation = useNavigation();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const theme = useTheme();
+
+ useEffect(() => {
+ const navigationState = navigation.getState();
+ const routes = navigationState.routes;
+ const currentRoute = routes[navigationState.index];
+ if (currentRoute && NAVIGATORS.CENTRAL_PANE_NAVIGATOR !== currentRoute.name && currentRoute.name !== SCREENS.HOME) {
+ return;
+ }
+
+ Welcome.show(routes, () => setIsModalOpen(true));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const closeModal = useCallback(() => {
+ Report.dismissEngagementModal();
+ setIsModalOpen(false);
+ }, []);
+
+ const completeModalAndClose = useCallback((message: string, choice: ValueOf) => {
+ Report.completeEngagementModal(message, choice);
+ setIsModalOpen(false);
+ Report.navigateToConciergeChat();
+ }, []);
+
+ const menuItems: MenuItemProps[] = useMemo(
+ () =>
+ Object.values(CONST.INTRO_CHOICES).map((choice) => {
+ const translationKey = `purposeForExpensify.${choice}` as const;
+ return {
+ key: translationKey,
+ title: translate(translationKey),
+ icon: menuIcons[choice],
+ iconRight: Expensicons.ArrowRight,
+ onPress: () => completeModalAndClose(messageCopy[choice], choice),
+ shouldShowRightIcon: true,
+ numberOfLinesTitle: 2,
+ };
+ }),
+ [completeModalAndClose, translate],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {translate('purposeForExpensify.welcomeMessage')}
+
+ {translate('purposeForExpensify.welcomeSubtitle')}
+
+
+
+
+
+ );
+}
+
+PurposeForUsingExpensifyModal.displayName = 'PurposeForUsingExpensifyModal';
+
+export default PurposeForUsingExpensifyModal;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b6da38df21a01..8ee92156bc601 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2073,6 +2073,14 @@ export default {
},
copyReferralLink: 'Copy invite link',
},
+ purposeForExpensify: {
+ [CONST.INTRO_CHOICES.TRACK]: 'Track business spend for taxes',
+ [CONST.INTRO_CHOICES.SUBMIT]: 'Get paid back by my employer',
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends',
+ welcomeMessage: 'Welcome to Expensify',
+ welcomeSubtitle: 'What would you like to do?',
+ },
violations: {
allTagLevelsRequired: 'All tags required',
autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 2478c8ba8bd2a..858fe29a8faf9 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2561,6 +2561,14 @@ export default {
},
copyReferralLink: 'Copiar enlace de invitación',
},
+ purposeForExpensify: {
+ [CONST.INTRO_CHOICES.TRACK]: 'Seguimiento de los gastos de empresa para fines fiscales',
+ [CONST.INTRO_CHOICES.SUBMIT]: 'Reclamar gastos a mi empleador',
+ [CONST.INTRO_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
+ [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chatea y divide gastos con tus amigos',
+ welcomeMessage: 'Bienvenido a Expensify',
+ welcomeSubtitle: '¿Qué te gustaría hacer?',
+ },
violations: {
allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
index a3fe1c657f34f..5462b6c0ce4e7 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
@@ -27,19 +27,21 @@ function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
we have 30px draggable ba at the top and the rest of the dimmed area is clickable. On other devices,
everything behaves normally like one big pressable */}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index d48567ebdaf39..63ab0f5736e06 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -2496,12 +2496,13 @@ function getParsedComment(text: string): string {
return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text);
}
-function buildOptimisticAddCommentReportAction(text?: string, file?: File): OptimisticReportAction {
+function buildOptimisticAddCommentReportAction(text?: string, file?: File, actorAccountID?: number): OptimisticReportAction {
const parser = new ExpensiMark();
const commentText = getParsedComment(text ?? '');
const isAttachment = !text && file !== undefined;
const attachmentInfo = isAttachment ? file : {};
const htmlForNewComment = isAttachment ? CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML : commentText;
+ const accountID = actorAccountID ?? currentUserAccountID;
// Remove HTML from text when applying optimistic offline comment
const textForNewComment = isAttachment ? CONST.ATTACHMENT_MESSAGE_TEXT : parser.htmlToText(htmlForNewComment);
@@ -2510,16 +2511,16 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: File): Opti
reportAction: {
reportActionID: NumberUtils.rand64(),
actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
- actorAccountID: currentUserAccountID,
+ actorAccountID: accountID,
person: [
{
style: 'strong',
- text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail,
+ text: allPersonalDetails?.[accountID ?? -1]?.displayName ?? currentUserEmail,
type: 'TEXT',
},
],
automatic: false,
- avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID),
+ avatar: allPersonalDetails?.[accountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(accountID),
created: DateUtils.getDBTimeWithSkew(),
message: [
{
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 2ac85dfafa27f..a3cd8f52a411f 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -2499,6 +2499,115 @@ function getReportPrivateNote(reportID: string) {
API.read('GetReportPrivateNote', parameters, {optimisticData, successData, failureData});
}
+/**
+ * Completes the engagement modal that new NewDot users see when they first sign up/log in by doing the following:
+ *
+ * - Sets the introSelected NVP to the choice the user made
+ * - Creates an optimistic report comment from concierge
+ */
+function completeEngagementModal(text: string, choice: ValueOf) {
+ const commandName = 'CompleteEngagementModal';
+ const conciergeAccountID = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE])[0];
+ const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, conciergeAccountID);
+ const reportCommentAction: OptimisticAddCommentReportAction = reportComment.reportAction;
+ const lastComment = reportCommentAction?.message?.[0];
+ const lastCommentText = ReportUtils.formatReportLastMessageText(lastComment?.text ?? '');
+ const reportCommentText = reportComment.commentText;
+ const currentTime = DateUtils.getDBTime();
+
+ const optimisticReport: Partial = {
+ lastVisibleActionCreated: currentTime,
+ lastMessageTranslationKey: lastComment?.translationKey ?? '',
+ lastMessageText: lastCommentText,
+ lastMessageHtml: lastCommentText,
+ lastActorAccountID: currentUserAccountID,
+ lastReadTime: currentTime,
+ };
+
+ const conciergeChatReport = ReportUtils.getChatByParticipants([conciergeAccountID]);
+ conciergeChatReportID = conciergeChatReport?.reportID;
+
+ const report = ReportUtils.getReport(conciergeChatReportID);
+
+ if (!isEmptyObject(report) && ReportUtils.getReportNotificationPreference(report) === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) {
+ optimisticReport.notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS;
+ }
+
+ // Optimistically add the new actions to the store before waiting to save them to the server
+ const optimisticReportActions: OnyxCollection = {};
+ if (reportCommentAction?.reportActionID) {
+ optimisticReportActions[reportCommentAction.reportActionID] = reportCommentAction;
+ }
+
+ type CompleteEngagementParameters = {
+ reportID: string;
+ reportActionID?: string;
+ commentReportActionID?: string | null;
+ reportComment?: string;
+ engagementChoice: string;
+ timezone?: string;
+ };
+
+ const parameters: CompleteEngagementParameters = {
+ reportID: conciergeChatReportID ?? '',
+ reportActionID: reportCommentAction.reportActionID,
+ reportComment: reportCommentText,
+ engagementChoice: choice,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${conciergeChatReportID}`,
+ value: optimisticReport,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeChatReportID}`,
+ value: optimisticReportActions as ReportActions,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_INTRO_SELECTED,
+ value: {choice},
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${conciergeChatReportID}`,
+ value: {[reportCommentAction.reportActionID ?? '']: {pendingAction: null}},
+ },
+ ];
+
+ API.write(commandName, parameters, {
+ optimisticData,
+ successData,
+ });
+ notifyNewAction(conciergeChatReportID ?? '', reportCommentAction.actorAccountID, reportCommentAction.reportActionID);
+}
+
+function dismissEngagementModal() {
+ const commandName = 'SetNameValuePair';
+ const parameters = {
+ name: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL,
+ value: true,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL,
+ value: true,
+ },
+ ];
+
+ API.write(commandName, parameters, {
+ optimisticData,
+ });
+}
+
/** Loads necessary data for rendering the RoomMembersPage */
function openRoomMembersPage(reportID: string) {
type OpenRoomMembersPageParameters = {
@@ -2709,6 +2818,8 @@ export {
hasErrorInPrivateNotes,
getOlderActions,
getNewerActions,
+ completeEngagementModal,
+ dismissEngagementModal,
openRoomMembersPage,
savePrivateNotesDraft,
getDraftPrivateNote,
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index 3e3cba49480d5..3f6b2dc99a8fa 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -1,13 +1,16 @@
+import type {NavigationState} from '@react-navigation/native';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
+import type {RootStackParamList} from '@navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type OnyxPolicy from '@src/types/onyx/Policy';
import type Report from '@src/types/onyx/Report';
+import type {EmptyObject} from '@src/types/utils/EmptyObject';
import * as Policy from './Policy';
let resolveIsReadyPromise: (value?: Promise) => void | undefined;
@@ -16,20 +19,11 @@ let isReadyPromise = new Promise((resolve) => {
});
let isFirstTimeNewExpensifyUser: boolean | undefined;
+let hasDismissedModal: boolean | undefined;
+let hasSelectedChoice: boolean | undefined;
let isLoadingReportData = true;
let currentUserAccountID: number | undefined;
-type Route = {
- name: string;
- params?: {path: string; exitTo?: string; openOnAdminRoom?: boolean};
-};
-
-type ShowParams = {
- routes: Route[];
- showCreateMenu?: () => void;
- showPopoverMenu?: () => boolean;
-};
-
/**
* Check that a few requests have completed so that the welcome action can proceed:
*
@@ -38,7 +32,7 @@ type ShowParams = {
* - Whether we have loaded all reports the server knows about
*/
function checkOnReady() {
- if (isFirstTimeNewExpensifyUser === undefined || isLoadingReportData) {
+ if (isFirstTimeNewExpensifyUser === undefined || isLoadingReportData || hasSelectedChoice === undefined || hasDismissedModal === undefined) {
return;
}
@@ -58,6 +52,26 @@ Onyx.connect({
},
});
+Onyx.connect({
+ key: ONYXKEYS.NVP_INTRO_SELECTED,
+ initWithStoredValues: true,
+ callback: (value) => {
+ hasSelectedChoice = !!value;
+
+ checkOnReady();
+ },
+});
+
+Onyx.connect({
+ key: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL,
+ initWithStoredValues: true,
+ callback: (value) => {
+ hasDismissedModal = value ?? false;
+
+ checkOnReady();
+ },
+});
+
Onyx.connect({
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
initWithStoredValues: false,
@@ -80,7 +94,7 @@ Onyx.connect({
},
});
-const allPolicies: OnyxCollection = {};
+const allPolicies: OnyxCollection | EmptyObject = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,
callback: (val, key) => {
@@ -111,7 +125,7 @@ Onyx.connect({
/**
* Shows a welcome action on first login
*/
-function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => false}: ShowParams) {
+function show(routes: NavigationState['routes'], showEngagementModal = () => {}) {
isReadyPromise.then(() => {
if (!isFirstTimeNewExpensifyUser) {
return;
@@ -119,16 +133,14 @@ function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => false}
// If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global
// create menu right now. We should also stay on the workspace page if that is our destination.
- const topRoute = routes.length > 0 ? routes[routes.length - 1] : undefined;
- const isWorkspaceRoute = topRoute !== undefined && topRoute.name === SCREENS.RIGHT_MODAL.SETTINGS && topRoute.params?.path.includes('workspace');
- const transitionRoute = routes.find((route) => route.name === SCREENS.TRANSITION_BETWEEN_APPS);
- const exitingToWorkspaceRoute = transitionRoute?.params?.exitTo === 'workspace/new';
- const openOnAdminRoom = topRoute?.params?.openOnAdminRoom ?? false;
- const isDisplayingWorkspaceRoute = isWorkspaceRoute ?? exitingToWorkspaceRoute;
+ const transitionRoute = routes.find(
+ (route): route is NavigationState>['routes'][number] => route.name === SCREENS.TRANSITION_BETWEEN_APPS,
+ );
+ const isExitingToWorkspaceRoute = transitionRoute?.params?.exitTo === 'workspace/new';
// If we already opened the workspace settings or want the admin room to stay open, do not
// navigate away to the workspace chat report
- const shouldNavigateToWorkspaceChat = !isDisplayingWorkspaceRoute && !openOnAdminRoom;
+ const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute;
const workspaceChatReport = Object.values(allReports ?? {}).find((report) => {
if (report) {
@@ -137,7 +149,7 @@ function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => false}
return false;
});
- if (workspaceChatReport ?? openOnAdminRoom) {
+ if (workspaceChatReport) {
// This key is only updated when we call ReconnectApp, setting it to false now allows the user to navigate normally instead of always redirecting to the workspace chat
Onyx.set(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, false);
}
@@ -147,19 +159,17 @@ function show({routes, showCreateMenu = () => {}, showPopoverMenu = () => false}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReport.reportID));
}
- // If showPopoverMenu exists and returns true then it opened the Popover Menu successfully, and we can update isFirstTimeNewExpensifyUser
- // so the Welcome logic doesn't run again
- if (showPopoverMenu?.()) {
- isFirstTimeNewExpensifyUser = false;
- }
+ // New user has been redirected to their workspace chat, and we won't show them the engagement modal.
+ // So we update isFirstTimeNewExpensifyUser to prevent the Welcome logic from running again
+ isFirstTimeNewExpensifyUser = false;
return;
}
// If user is not already an admin of a free policy and we are not navigating them to their workspace or creating a new workspace via workspace/new then
- // we will show the create menu.
- if (!Policy.isAdminOfFreePolicy(allPolicies ?? undefined) && !isDisplayingWorkspaceRoute) {
- showCreateMenu();
+ // we will show the engagement modal.
+ if (!Policy.isAdminOfFreePolicy(allPolicies ?? undefined) && !isExitingToWorkspaceRoute && !hasSelectedChoice && !hasDismissedModal && Object.keys(allPolicies ?? {}).length === 1) {
+ showEngagementModal();
}
// Update isFirstTimeNewExpensifyUser so the Welcome logic doesn't run again
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index 9aeabefd645dd..bbcdc5cebef4f 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -1,4 +1,3 @@
-import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
@@ -20,12 +19,9 @@ import * as IOU from '@userActions/IOU';
import * as Policy from '@userActions/Policy';
import * as Session from '@userActions/Session';
import * as Task from '@userActions/Task';
-import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
-import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
/**
* @param {Object} [policy]
@@ -142,18 +138,6 @@ function FloatingActionButtonAndPopover(props) {
}
};
- useEffect(() => {
- const navigationState = props.navigation.getState();
- const routes = lodashGet(navigationState, 'routes', []);
- const currentRoute = routes[navigationState.index];
- if (currentRoute && ![NAVIGATORS.CENTRAL_PANE_NAVIGATOR, SCREENS.HOME].includes(currentRoute.name)) {
- return;
- }
-
- Welcome.show({routes, showCreateMenu});
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.isLoading]);
-
useEffect(() => {
if (!didScreenBecomeInactive()) {
return;
diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.js
index 0b4c520c78a2c..e823d24b87fe8 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.js
@@ -1,4 +1,5 @@
import React, {useCallback, useRef} from 'react';
+import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal';
import useWindowDimensions from '@hooks/useWindowDimensions';
import FreezeWrapper from '@libs/Navigation/FreezeWrapper';
import BaseSidebarScreen from './BaseSidebarScreen';
@@ -44,6 +45,7 @@ function SidebarScreen(props) {
onShowCreateMenu={createDragoverListener}
onHideCreateMenu={removeDragoverListener}
/>
+
);
diff --git a/src/pages/home/sidebar/SidebarScreen/index.native.js b/src/pages/home/sidebar/SidebarScreen/index.native.js
index 36724c02d278f..7f36e4ebfa22d 100755
--- a/src/pages/home/sidebar/SidebarScreen/index.native.js
+++ b/src/pages/home/sidebar/SidebarScreen/index.native.js
@@ -1,4 +1,5 @@
import React from 'react';
+import PurposeForUsingExpensifyModal from '@components/PurposeForUsingExpensifyModal';
import useWindowDimensions from '@hooks/useWindowDimensions';
import FreezeWrapper from '@libs/Navigation/FreezeWrapper';
import BaseSidebarScreen from './BaseSidebarScreen';
@@ -14,6 +15,7 @@ function SidebarScreen(props) {
{...props}
>
+
);
diff --git a/src/styles/index.ts b/src/styles/index.ts
index aace13c345946..9bfe407593df4 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2875,6 +2875,10 @@ const styles = (theme: ThemeColors) =>
outlineStyle: 'none',
},
+ boxShadowNone: {
+ boxShadow: 'none',
+ },
+
cardStyleNavigator: {
overflow: 'hidden',
height: '100%',
diff --git a/src/types/onyx/IntroSelected.ts b/src/types/onyx/IntroSelected.ts
new file mode 100644
index 0000000000000..f0047ac134ee2
--- /dev/null
+++ b/src/types/onyx/IntroSelected.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type IntroSelected = {
+ /** The choice that the user selected in the engagement modal */
+ choice: ValueOf;
+};
+
+export default IntroSelected;
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index e6d6c27fc8181..f1179d33fb5c8 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -14,6 +14,7 @@ import type Form from './Form';
import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import type {FundList} from './Fund';
import type Fund from './Fund';
+import type IntroSelected from './IntroSelected';
import type IOU from './IOU';
import type Locale from './Locale';
import type {LoginList} from './Login';
@@ -85,6 +86,7 @@ export type {
FrequentlyUsedEmoji,
Fund,
FundList,
+ IntroSelected,
IOU,
Locale,
Login,
diff --git a/tests/unit/CalendarPickerTest.js b/tests/unit/CalendarPickerTest.js
index e1c11fbb8ca84..3aab3a13c1c32 100644
--- a/tests/unit/CalendarPickerTest.js
+++ b/tests/unit/CalendarPickerTest.js
@@ -7,6 +7,7 @@ import DateUtils from '../../src/libs/DateUtils';
const monthNames = DateUtils.getMonthNames(CONST.LOCALES.EN);
jest.mock('@react-navigation/native', () => ({
+ ...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({navigate: jest.fn()}),
createNavigationContainerRef: jest.fn(),
}));