-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Friendly Sign-Off to Concierge Onboarding Messages for a More Inviting LHN Preview #54144
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! 🙏 |
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 Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)myBool && <MyComponent />
.src/languages/*
files and using the translation methodSTYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)Design
label and/or tagged@Expensify/design
so the design team can review the changes.ScrollView
component to make it scrollable when more elements are added to the page.main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.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