Add Friendly Sign-Off to Concierge Onboarding Messages for a More Inviting LHN Preview#54144
Add Friendly Sign-Off to Concierge Onboarding Messages for a More Inviting LHN Preview#54144jasperhuangg merged 5 commits intoExpensify:mainfrom
Conversation
|
@shubham1206agra Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
@shubham1206agra Friendly bump on this! |
|
@jasperhuangg I am waiting for onboarding flows to be fixed. |
|
@ugogiordano Please merge main |
|
@shubham1206agra done |
Screen.Recording.2024-12-20.at.12.41.15.PM.mov@ugogiordano The message is in wrong place. |
|
@shubham1206agra Based on the issue description, the goal seems to be replacing the "task for ..." message, which currently appears in the Admin chat instead of the Concierge chat in some cases, as shown in your video. Does replacing the "Welcome" message in the Concierge chat make sense when the onboarding choice is "Manage my team"? This would still leave the "task for ..." message in the Admins chat. @jamesdeanexpensify What are your thoughts? |
|
Great catch @ugogiordano - we'd want to have the "It's great to meet you!" wherever the onboarding tasks show, so it covers up the task message. |
|
@ugogiordano Use this implementation instead. Please merge main first. function prepareOnboardingOptimisticData(
engagementChoice: OnboardingPurpose,
data: ValueOf<typeof CONST.ONBOARDING_MESSAGES>,
adminsChatReportID?: string,
onboardingPolicyID?: string,
userReportedIntegration?: OnboardingAccounting,
wasInvited?: boolean,
) {
// If the user has the "combinedTrackSubmit" beta enabled we'll show different tasks for track and submit expense.
if (Permissions.canUseCombinedTrackSubmit()) {
if (engagementChoice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
// eslint-disable-next-line no-param-reassign
data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.PERSONAL_SPEND];
}
if (engagementChoice === CONST.ONBOARDING_CHOICES.EMPLOYER || engagementChoice === CONST.ONBOARDING_CHOICES.SUBMIT) {
// eslint-disable-next-line no-param-reassign
data = CONST.COMBINED_TRACK_SUBMIT_ONBOARDING_MESSAGES[CONST.ONBOARDING_CHOICES.SUBMIT];
}
}
// 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('+');
const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : '';
const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : ReportUtils.getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]);
const {reportID: targetChatReportID = '', policyID: targetChatPolicyID = ''} = targetChatReport ?? {};
const assignedGuideEmail = getPolicy(targetChatPolicyID)?.assignedGuide?.email ?? 'Setup Specialist';
const assignedGuidePersonalDetail = Object.values(allPersonalDetails ?? {}).find((personalDetail) => personalDetail?.login === assignedGuideEmail);
let assignedGuideAccountID: number;
if (assignedGuidePersonalDetail) {
assignedGuideAccountID = assignedGuidePersonalDetail.accountID;
} else {
assignedGuideAccountID = generateAccountID(assignedGuideEmail);
Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
[assignedGuideAccountID]: {
login: assignedGuideEmail,
displayName: assignedGuideEmail,
},
});
}
const actorAccountID = shouldPostTasksInAdminsRoom ? assignedGuideAccountID : CONST.ACCOUNT_ID.CONCIERGE;
// Text message
const textComment = ReportUtils.buildOptimisticAddCommentReportAction(data.message, undefined, actorAccountID, 1);
const textCommentAction: OptimisticAddCommentReportAction = textComment.reportAction;
const textMessage: AddCommentOrAttachementParams = {
reportID: targetChatReportID,
reportActionID: textCommentAction.reportActionID,
reportComment: textComment.commentText,
};
let videoCommentAction: OptimisticAddCommentReportAction | null = null;
let videoMessage: AddCommentOrAttachementParams | null = null;
if ('video' in data && data.video) {
const videoComment = ReportUtils.buildOptimisticAddCommentReportAction(CONST.ATTACHMENT_MESSAGE_TEXT, undefined, actorAccountID, 2);
videoCommentAction = videoComment.reportAction;
videoMessage = {
reportID: targetChatReportID,
reportActionID: videoCommentAction.reportActionID,
reportComment: videoComment.commentText,
};
}
const tasksData = data.tasks
.filter((task) => {
if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) {
return false;
}
if (['addAccountingIntegration', 'setupCategoriesAndTags'].includes(task.type) && !userReportedIntegration) {
return false;
}
return true;
})
.map((task, index) => {
const taskDescription =
typeof task.description === 'function'
? task.description({
adminsRoomLink: `${environmentURL}/${ROUTES.REPORT_WITH_ID.getRoute(adminsChatReportID ?? '-1')}`,
workspaceCategoriesLink: `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(onboardingPolicyID ?? '-1')}`,
workspaceMembersLink: `${environmentURL}/${ROUTES.WORKSPACE_MEMBERS.getRoute(onboardingPolicyID ?? '-1')}`,
workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID ?? '-1')}`,
navatticURL: getNavatticURL(environment, engagementChoice),
integrationName,
workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID ?? '-1')}`,
workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID ?? '-1')}`,
})
: task.description;
const taskTitle =
typeof task.title === 'function'
? task.title({
integrationName,
})
: task.title;
const currentTask = ReportUtils.buildOptimisticTaskReport(
actorAccountID,
currentUserAccountID,
targetChatReportID,
taskTitle,
taskDescription,
targetChatPolicyID,
CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
);
const emailCreatingAction =
engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? allPersonalDetails?.[actorAccountID]?.login ?? CONST.EMAIL.CONCIERGE : CONST.EMAIL.CONCIERGE;
const taskCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(emailCreatingAction);
const taskReportAction = ReportUtils.buildOptimisticTaskCommentReportAction(
currentTask.reportID,
taskTitle,
0,
`task for ${taskTitle}`,
targetChatReportID,
actorAccountID,
index + 3,
);
currentTask.parentReportActionID = taskReportAction.reportAction.reportActionID;
const completedTaskReportAction = task.autoCompleted
? ReportUtils.buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, 'marked as complete', actorAccountID, 2)
: null;
return {
task,
currentTask,
taskCreatedAction,
taskReportAction,
taskDescription: currentTask.description,
completedTaskReportAction,
};
});
// Sign-off welcome message
const welcomeSignOffComment = ReportUtils.buildOptimisticAddCommentReportAction(
Localize.translateLocal('onboarding.welcomeSignOffTitle'),
undefined,
actorAccountID,
tasksData.length + 3,
);
const welcomeSignOffCommentAction: OptimisticAddCommentReportAction = welcomeSignOffComment.reportAction;
const welcomeSignOffMessage = {
reportID: targetChatReportID,
reportActionID: welcomeSignOffCommentAction.reportActionID,
reportComment: welcomeSignOffComment.commentText,
};
const tasksForParameters = tasksData.map<TaskForParameters>(({task, currentTask, taskCreatedAction, taskReportAction, taskDescription, completedTaskReportAction}) => ({
type: 'task',
task: task.type,
taskReportID: currentTask.reportID,
parentReportID: currentTask.parentReportID ?? '-1',
parentReportActionID: taskReportAction.reportAction.reportActionID,
assigneeChatReportID: '',
createdTaskReportActionID: taskCreatedAction.reportActionID,
completedTaskReportActionID: completedTaskReportAction?.reportActionID ?? undefined,
title: currentTask.reportName ?? '',
description: taskDescription ?? '',
}));
const hasOutstandingChildTask = tasksData.some((task) => !task.completedTaskReportAction);
const tasksForOptimisticData = tasksData.reduce<OnyxUpdate[]>((acc, {currentTask, taskCreatedAction, taskReportAction, taskDescription, completedTaskReportAction}) => {
acc.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[taskReportAction.reportAction.reportActionID]: taskReportAction.reportAction as ReportAction,
},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`,
value: {
...currentTask,
description: taskDescription,
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
managerID: currentUserAccountID,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${currentTask.reportID}`,
value: {
isOptimisticReport: true,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`,
value: {
[taskCreatedAction.reportActionID]: taskCreatedAction as ReportAction,
},
},
);
if (completedTaskReportAction) {
acc.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`,
value: {
[completedTaskReportAction.reportActionID]: completedTaskReportAction as ReportAction,
},
});
acc.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`,
value: {
stateNum: CONST.REPORT.STATE_NUM.APPROVED,
statusNum: CONST.REPORT.STATUS_NUM.APPROVED,
managerID: currentUserAccountID,
},
});
}
return acc;
}, []);
const tasksForFailureData = tasksData.reduce<OnyxUpdate[]>((acc, {currentTask, taskReportAction}) => {
acc.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[taskReportAction.reportAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
} as ReportAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`,
value: null,
},
);
return acc;
}, []);
const tasksForSuccessData = tasksData.reduce<OnyxUpdate[]>((acc, {currentTask, taskCreatedAction, taskReportAction, completedTaskReportAction}) => {
acc.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[taskReportAction.reportAction.reportActionID]: {pendingAction: null},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${currentTask.reportID}`,
value: {
pendingFields: {
createChat: null,
reportName: null,
description: null,
managerID: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${currentTask.reportID}`,
value: {
isOptimisticReport: false,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`,
value: {
[taskCreatedAction.reportActionID]: {pendingAction: null},
},
},
);
if (completedTaskReportAction) {
acc.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentTask.reportID}`,
value: {
[completedTaskReportAction.reportActionID]: {pendingAction: null},
},
});
}
return acc;
}, []);
const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData];
const lastVisibleActionCreated = welcomeSignOffCommentAction.created;
optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`,
value: {
lastMentionedTime: DateUtils.getDBTime(),
hasOutstandingChildTask,
lastVisibleActionCreated,
lastActorAccountID: actorAccountID,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_INTRO_SELECTED,
value: {choice: engagementChoice},
},
);
// 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) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[textCommentAction.reportActionID]: textCommentAction as ReportAction,
},
});
}
if (!wasInvited) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_ONBOARDING,
value: {hasCompletedGuidedSetupFlow: true},
});
}
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) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[textCommentAction.reportActionID]: {pendingAction: null},
},
});
}
let failureReport: Partial<Report> = {
lastMessageText: '',
lastVisibleActionCreated: '',
hasOutstandingChildTask: false,
};
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`];
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
const {lastMessageText = ''} = ReportActionsUtils.getLastVisibleMessage(targetChatReportID, canUserPerformWriteAction);
if (lastMessageText) {
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(targetChatReportID, canUserPerformWriteAction);
const prevLastVisibleActionCreated = lastVisibleAction?.created;
const lastActorAccountID = lastVisibleAction?.actorAccountID;
failureReport = {
lastMessageText,
lastVisibleActionCreated: prevLastVisibleActionCreated,
lastActorAccountID,
};
}
const failureData: OnyxUpdate[] = [...tasksForFailureData];
failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`,
value: failureReport,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_INTRO_SELECTED,
value: {choice: 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) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[textCommentAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
} as ReportAction,
},
});
}
if (!wasInvited) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_ONBOARDING,
value: {hasCompletedGuidedSetupFlow: false},
});
}
if (userReportedIntegration) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${onboardingPolicyID}`,
value: {
areConnectionsEnabled: true,
pendingFields: {
areConnectionsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${onboardingPolicyID}`,
value: {
pendingFields: {
areConnectionsEnabled: null,
},
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${onboardingPolicyID}`,
value: {
areConnectionsEnabled: getPolicy(onboardingPolicyID)?.areConnectionsEnabled,
pendingFields: {
areConnectionsEnabled: 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
const guidedSetupData: GuidedSetupData = shouldPostTasksInAdminsRoom ? [] : [{type: 'message', ...textMessage}];
if (!shouldPostTasksInAdminsRoom && 'video' in data && data.video && videoCommentAction && videoMessage) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[videoCommentAction.reportActionID]: videoCommentAction as ReportAction,
},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[videoCommentAction.reportActionID]: {pendingAction: null},
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[videoCommentAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
} as ReportAction,
},
});
guidedSetupData.push({type: 'video', ...data.video, ...videoMessage});
}
if (engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM) {
const selfDMReportID = ReportUtils.findSelfDMReportID();
const selfDMReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`];
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`,
value: {
isPinned: false,
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`,
value: {
isPinned: selfDMReport?.isPinned,
},
});
}
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[welcomeSignOffCommentAction.reportActionID]: welcomeSignOffCommentAction as ReportAction,
},
});
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[welcomeSignOffCommentAction.reportActionID]: {pendingAction: null},
},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[welcomeSignOffCommentAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
} as ReportAction,
},
});
guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage});
return {optimisticData, successData, failureData, guidedSetupData, actorAccountID};
} |
|
@shubham1206agra done. |
Reviewer Checklist
Screenshots/VideosAndroid: NativeAndroid: mWeb ChromeScreen.Recording.2024-12-29.at.9.46.27.PM.moviOS: NativeScreen.Recording.2024-12-29.at.10.08.25.PM.moviOS: mWeb SafariScreen.Recording.2024-12-29.at.9.40.22.PM.movMacOS: Chrome / SafariScreen.Recording.2024-12-29.at.9.03.35.PM.movMacOS: DesktopScreen.Recording.2024-12-29.at.9.53.22.PM.mov |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/jasperhuangg in version: 9.0.80-1 🚀
|
|
FYI it would be nice to have your PR named something useful - like explaining what you fixed / what the purpose of the PR is 🙏 thanks! |
|
@Beamanator done. |
|
amazing, thanks so much! 🙏 |
|
🚀 Deployed to production by https://github.com/puneetlath in version: 9.0.80-6 🚀
|
Details
Fixed Issues
$ #51501
PROPOSAL: #51501 (comment)
Tests
Offline tests
QA Steps
Here is a video demonstrating the steps to reproduce the issue.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)myBool && <MyComponent />.src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Screen.Recording.2024-12-14.at.01.47.34.mov
Android: mWeb Chrome
Screen.Recording.2024-12-13.at.23.40.53.mov
iOS: Native
Screen.Recording.2024-12-13.at.18.26.20.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
MacOS: Desktop
Screen.Recording.2024-12-13.at.00.32.22.mov