Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/hooks/useAutoCreateSubmitWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ function useAutoCreateSubmitWorkspace() {
onboardingPolicyID: newPolicyID,
introSelected,
isSelfTourViewed,
betas,
});

setOnboardingAdminsChatReportID();
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useAutoCreateTrackWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ function useAutoCreateTrackWorkspace() {
shouldWaitForRHPVariantInitialization: isSidePanelReportSupported,
introSelected,
isSelfTourViewed,
betas,
});

if (isSidePanelReportSupported) {
Expand Down
1 change: 0 additions & 1 deletion src/libs/API/parameters/CompleteGuidedSetupParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type CompleteGuidedSetupParams = {
policyID?: string;
selfDMReportID?: string;
selfDMCreatedReportActionID?: string;
bespokeWelcomeMessage?: string;
optimisticConciergeReportActionID?: string;
/** Feature ids the user toggled on the InterestedFeatures onboarding page; shapes the #admins welcome followups on this single request. */
selectedInterestedFeatures?: string;
Expand Down
1 change: 0 additions & 1 deletion src/libs/API/parameters/CreateWorkspaceParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type CreateWorkspaceParams = {
features?: string;
shouldAddGuideWelcomeMessage?: boolean;
areDistanceRatesEnabled?: boolean;
bespokeWelcomeMessage?: string;
optimisticConciergeReportActionID?: string;
};

Expand Down
169 changes: 12 additions & 157 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type PolicyData from '@hooks/usePolicyData/types';
import type {PolicyTagList} from '@pages/workspace/tags/types';
import type {ThemeColors} from '@styles/theme/types';
import type {IOUAction, IOURequestType, IOUType, OnboardingAccounting} from '@src/CONST';
import CONST, {TASK_TO_FEATURE} from '@src/CONST';
import CONST from '@src/CONST';
import type {ParentNavigationSummaryParams} from '@src/languages/params';
import type {TranslationPaths} from '@src/languages/types';
import NAVIGATORS from '@src/NAVIGATORS';
Expand All @@ -36,7 +36,6 @@ import SCREENS from '@src/SCREENS';
import type {
BankAccountList,
Beta,
BetaConfiguration,
BillingGraceEndPeriod,
IntroSelected,
OnyxInputOrEntry,
Expand Down Expand Up @@ -1140,12 +1139,6 @@ Onyx.connect({
},
});

let betaConfiguration: OnyxEntry<BetaConfiguration> = {};
Onyx.connect({
key: ONYXKEYS.BETA_CONFIGURATION,
callback: (value) => (betaConfiguration = value ?? {}),
});

let deprecatedAllTransactions: OnyxCollection<Transaction> = {};
let deprecatedReportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({
Expand Down Expand Up @@ -11584,55 +11577,14 @@ type PrepareOnboardingOnyxDataParams = {
userReportedIntegration?: OnboardingAccounting;
wasInvited?: boolean;
companySize: OnboardingCompanySize | undefined;
selectedInterestedFeatures?: string[];
isInvitedAccountant?: boolean;
onboardingPurposeSelected?: OnboardingPurpose;
betas: OnyxEntry<Beta[]>;
// TODO: isSelfTourViewed will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66424
isSelfTourViewed?: boolean;
// TODO: hasCompletedGuidedSetupFlow will be required eventually. Refactor issue: https://github.com/Expensify/App/issues/66424
hasCompletedGuidedSetupFlow?: boolean;
};

function getBespokeWelcomeMessage(companySize: OnboardingCompanySize | undefined, userReportedIntegration?: OnboardingAccounting): string {
// Use markdown (not HTML) because buildOptimisticAddCommentReportAction -> getParsedComment
// escapes HTML entities before parsing, so raw HTML tags would render as literal text.
const welcomeHeader = "# Your free trial has started! Let's get you set up.\n👋 Hey there! I'm your Expensify setup specialist. ";

let message = welcomeHeader;
switch (companySize) {
case CONST.ONBOARDING_COMPANY_SIZE.MEDIUM:
case CONST.ONBOARDING_COMPANY_SIZE.LARGE:
message +=
'For an organization your size, the fastest path to value is setting up approval workflows, ' +
'connecting your accounting software, and rolling out the Expensify Card to your team. ' +
"I'm here to walk you through each step — just ask!";
break;
case CONST.ONBOARDING_COMPANY_SIZE.SMALL:
case CONST.ONBOARDING_COMPANY_SIZE.MEDIUM_SMALL:
message +=
'For a growing team like yours, the fastest way to get value is to set up expense categories, ' +
'configure approval workflows, and invite your team members. ' +
"I'm here to walk you through each step — just ask!";
break;
default:
message +=
'For a small team like yours, the fastest way to get value is to set up a few expense categories, ' +
'invite your team members, and have them start snapping receipts right away. ' +
"I'm here to walk you through each step — just ask!";
break;
}

if (userReportedIntegration && userReportedIntegration !== 'other') {
const friendlyName = CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration as keyof typeof CONST.ONBOARDING_ACCOUNTING_MAPPING];
if (friendlyName) {
message += `\n\nSince you use ${friendlyName}, I can help you connect it so your expenses sync automatically — just say the word!`;
}
}

return message;
}

function prepareOnboardingOnyxData({
introSelected,
engagementChoice,
Expand All @@ -11642,11 +11594,9 @@ function prepareOnboardingOnyxData({
userReportedIntegration,
wasInvited,
companySize,
selectedInterestedFeatures,
isInvitedAccountant,
onboardingPurposeSelected,
isSelfTourViewed,
betas,
hasCompletedGuidedSetupFlow,
}: PrepareOnboardingOnyxDataParams) {
if (engagementChoice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
Expand All @@ -11659,15 +11609,10 @@ function prepareOnboardingOnyxData({
onboardingMessage = getOnboardingMessages().onboardingMessages[CONST.ONBOARDING_CHOICES.SUBMIT];
}

// Phase 1 cohort (MANAGE_TEAM + micro company size) bypasses the beta gate — the backend
// handles gating via NVP, so all micro users get followups without needing the beta flag.
// Includes MICRO_SMALL, MICRO_MEDIUM, and the deprecated MICRO for backwards compatibility.
const isPhase1Cohort =
companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO_SMALL || companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO_MEDIUM || companySize === CONST.ONBOARDING_COMPANY_SIZE.MICRO;
// Followups path: MANAGE_TEAM + (Phase 1 cohort OR suggestedFollowups beta). Reaches every
// MANAGE_TEAM cohort user, including `+` aliases and phone-primary sign-ups.
const shouldUseFollowupsInsteadOfTasks =
engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && (isPhase1Cohort || Permissions.isBetaEnabled(CONST.BETAS.SUGGESTED_FOLLOWUPS, betas, betaConfiguration));
// Every MANAGE_TEAM signup uses the bespoke direct-post path. The server generates the
// per-tier welcome body in `UserAPI::buildBespokeWelcomeMessage`, so the App no longer
// needs a cohort gate or the suggestedFollowups beta to opt in.
const shouldUseFollowupsInsteadOfTasks = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM;
Comment thread
marcochavezf marked this conversation as resolved.
// Post to #admins room when followups fire OR the existing tasks-in-admins predicate approves.
const shouldPostTasksInAdminsRoom = shouldUseFollowupsInsteadOfTasks || isPostingTasksInAdminsRoom(engagementChoice);
const adminsChatReport = deprecatedAllReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`];
Expand Down Expand Up @@ -11740,29 +11685,10 @@ function prepareOnboardingOnyxData({
reportComment: textComment.commentText,
};

// When using followups instead of tasks, generate a bespoke welcome message from Concierge.
// The frontend displays it optimistically; the server uses it to generate suggested followups.
let bespokeWelcomeMessage: string | undefined;
let optimisticConciergeReportActionID: string | undefined;
let bespokeAction: OptimisticReportAction | undefined;

if (shouldUseFollowupsInsteadOfTasks) {
const bespokeMarkdown = getBespokeWelcomeMessage(companySize, userReportedIntegration);
optimisticConciergeReportActionID = rand64();
// delegateAccountIDParam: will be threaded in PR 14; buildOptimisticAddCommentReportAction falls back to module-level Onyx.connect value (https://github.com/Expensify/App/issues/66425)
bespokeAction = buildOptimisticAddCommentReportAction({
text: bespokeMarkdown,
actorAccountID: CONST.ACCOUNT_ID.CONCIERGE,
createdOffset: 2,
reportID: targetChatReportID,
reportActionID: optimisticConciergeReportActionID,
delegateAccountIDParam: undefined,
});
// Reuse the HTML that buildOptimisticAddCommentReportAction already parsed via getParsedComment,
// so we avoid calling getParsedComment a second time with the same input.
// The backend passes this to the LLM as HTML for AddComment, which expects HTML.
bespokeWelcomeMessage = bespokeAction.commentText;
}
// Generate a deduplication ID for the server-side bespoke welcome. The server posts directly using
// this ID via addComment (idempotent on reportActionID), so we never add an optimistic action
// here — the real message arrives from the server without a flash.
const optimisticConciergeReportActionID: string | undefined = shouldUseFollowupsInsteadOfTasks ? rand64() : undefined;
Comment thread
marcochavezf marked this conversation as resolved.

let createWorkspaceTaskReportID;
let addExpenseApprovalsTaskReportID;
Expand All @@ -11772,41 +11698,13 @@ function prepareOnboardingOnyxData({
const tasks = shouldUseFollowupsInsteadOfTasks ? [] : onboardingMessage.tasks;
const tasksData = tasks
.filter((task) => {
if (engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM) {
if (!!selectedInterestedFeatures && TASK_TO_FEATURE[task.type] && !selectedInterestedFeatures.includes(TASK_TO_FEATURE[task.type])) {
return false;
}
}

if (([CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES, CONST.ONBOARDING_TASK_TYPE.SETUP_TAGS] as string[]).includes(task.type) && userReportedIntegration) {
return false;
}

if (([CONST.ONBOARDING_TASK_TYPE.ADD_ACCOUNTING_INTEGRATION, CONST.ONBOARDING_TASK_TYPE.SETUP_CATEGORIES_AND_TAGS] as string[]).includes(task.type) && !userReportedIntegration) {
return false;
}
type SkipViewTourOnboardingChoices =
| typeof CONST.ONBOARDING_CHOICES.SUBMIT
| typeof CONST.ONBOARDING_CHOICES.CHAT_SPLIT
| typeof CONST.ONBOARDING_CHOICES.PERSONAL_SPEND
| typeof CONST.ONBOARDING_CHOICES.EMPLOYER
| typeof CONST.ONBOARDING_CHOICES.TRACK_PERSONAL
| typeof CONST.ONBOARDING_CHOICES.MANAGE_TEAM;
if (
task.type === CONST.ONBOARDING_TASK_TYPE.VIEW_TOUR &&
[
CONST.ONBOARDING_CHOICES.EMPLOYER,
CONST.ONBOARDING_CHOICES.PERSONAL_SPEND,
CONST.ONBOARDING_CHOICES.TRACK_PERSONAL,
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;
}

// Exclude createWorkspace and viewTour tasks from #admin room, for test drive receivers,
// since these users already have them in concierge
if (
Expand All @@ -11832,8 +11730,7 @@ function prepareOnboardingOnyxData({
CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
targetChatType,
);
const emailCreatingAction =
engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? (allPersonalDetails?.[actorAccountID]?.login ?? CONST.EMAIL.CONCIERGE) : CONST.EMAIL.CONCIERGE;
const emailCreatingAction = CONST.EMAIL.CONCIERGE;
const taskCreatedAction = buildOptimisticCreatedReportAction({emailCreatingAction});
const taskReportAction = buildOptimisticTaskCommentReportAction(currentTask.reportID, taskTitle, 0, `task for ${taskTitle}`, targetChatReportID, actorAccountID, index + 3);
currentTask.parentReportActionID = taskReportAction.reportAction.reportActionID;
Expand Down Expand Up @@ -12063,7 +11960,7 @@ function prepareOnboardingOnyxData({
key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`,
value: {
hasOutstandingChildTask,
...(skipSignOff && !message ? {} : {lastVisibleActionCreated}),
...(shouldUseFollowupsInsteadOfTasks || (skipSignOff && !message) ? {} : {lastVisibleActionCreated}),
lastActorAccountID: actorAccountID,
},
Comment on lines +11963 to 11965
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid updating last actor when no action is posted

In the MANAGE_TEAM followups path, this merge now skips lastVisibleActionCreated but still writes lastActorAccountID, even though no optimistic report action is added for that actor in this request. That leaves report metadata in an inconsistent state (actor changed without a corresponding visible action), which can cause incorrect sidebar preview/unread attribution until the server message arrives, especially on slow/offline connections.

Useful? React with 👍 / 👎.

},
Expand All @@ -12089,26 +11986,6 @@ function prepareOnboardingOnyxData({
});
}

if (bespokeAction && optimisticConciergeReportActionID) {
optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[optimisticConciergeReportActionID]: bespokeAction.reportAction as ReportAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`,
value: {
lastVisibleActionCreated: bespokeAction.reportAction.created,
lastActorAccountID: CONST.ACCOUNT_ID.CONCIERGE,
},
},
);
}

if (!wasInvited) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -12131,16 +12008,6 @@ function prepareOnboardingOnyxData({
});
}

if (bespokeAction && optimisticConciergeReportActionID) {
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[optimisticConciergeReportActionID]: {pendingAction: null, isOptimisticAction: null},
},
});
}

let failureReport: Partial<Report> = {
lastMessageText: '',
lastVisibleActionCreated: '',
Expand Down Expand Up @@ -12195,18 +12062,6 @@ function prepareOnboardingOnyxData({
});
}

if (bespokeAction && optimisticConciergeReportActionID) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`,
value: {
[optimisticConciergeReportActionID]: {
errors: getMicroSecondOnyxErrorWithTranslationKey('report.genericAddCommentFailureMessage'),
} as ReportAction,
},
});
}

if (!wasInvited) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
Expand Down Expand Up @@ -12381,7 +12236,7 @@ function prepareOnboardingOnyxData({
});
}

return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters, bespokeWelcomeMessage, optimisticConciergeReportActionID};
return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters, optimisticConciergeReportActionID};
}

/**
Expand Down
1 change: 0 additions & 1 deletion src/libs/actions/IOU/PayMoneyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,6 @@ function completePaymentOnboarding(
companySize: introSelected?.companySize as OnboardingCompanySize,
introSelected,
isSelfTourViewed,
betas,
});
}

Expand Down
1 change: 0 additions & 1 deletion src/libs/actions/IOU/TrackExpense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1801,7 +1801,6 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep
onboardingMessage: getOnboardingMessages().onboardingMessages[CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER],
companySize: undefined,
isSelfTourViewed,
betas,
})?.guidedSetupData
: undefined;

Expand Down
14 changes: 2 additions & 12 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,8 @@ type BuildPolicyDataOptions = {
type?: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.SUBMIT;
// TODO: Make it required once we complete refactoring the buildPolicyData function to use isSelfTourViewed. Refactor issue: https://github.com/Expensify/App/issues/66424
isSelfTourViewed?: boolean;
betas: OnyxEntry<Beta[]>;
hasActiveAdminPolicies: boolean | undefined;
betas?: OnyxEntry<Beta[]>;
Comment thread
marcochavezf marked this conversation as resolved.
};

// TODO: Remove this type once we complete refactoring the buildPolicyData function to use isSelfTourViewed. Refactor issue: https://github.com/Expensify/App/issues/66424
Expand Down Expand Up @@ -2538,7 +2538,6 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
shouldCreateControlPolicy = false,
type,
isSelfTourViewed,
betas,
hasActiveAdminPolicies,
} = options;

Expand Down Expand Up @@ -3011,23 +3010,14 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
onboardingPurposeSelected,
companySize: companySize ?? (introSelected?.companySize as OnboardingCompanySize),
isSelfTourViewed,
betas,
});
if (!onboardingData) {
return {successData, optimisticData, failureData, params};
}
const {
guidedSetupData,
optimisticData: taskOptimisticData,
successData: taskSuccessData,
failureData: taskFailureData,
bespokeWelcomeMessage,
optimisticConciergeReportActionID,
} = onboardingData;
const {guidedSetupData, optimisticData: taskOptimisticData, successData: taskSuccessData, failureData: taskFailureData, optimisticConciergeReportActionID} = onboardingData;

params.guidedSetupData = JSON.stringify(guidedSetupData);
params.engagementChoice = engagementChoice;
params.bespokeWelcomeMessage = bespokeWelcomeMessage;
params.optimisticConciergeReportActionID = optimisticConciergeReportActionID;

optimisticData.push(...taskOptimisticData);
Expand Down
Loading
Loading