From 1002d3bafc17269174757e7a131ea193e69d4312 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 9 Jan 2025 22:32:23 +0530 Subject: [PATCH 01/94] add tasks for new user first workspace --- src/CONST.ts | 125 ++++++++++++------ .../API/parameters/CreateWorkspaceParams.ts | 1 + src/libs/actions/IOU.ts | 6 +- src/libs/actions/Policy/Policy.ts | 36 ++++- src/libs/actions/Report.ts | 80 ++++++----- src/types/onyx/IntroSelected.ts | 3 + 6 files changed, 176 insertions(+), 75 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 69d01c6b09d5..22d5d0ab1cea 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -72,6 +72,7 @@ const selectableOnboardingChoices = { const backendOnboardingChoices = { ADMIN: 'newDotAdmin', SUBMIT: 'newDotSubmit', + TRACK_WORKSPACE: 'newDotTrackWorkspace', } as const; const onboardingChoices = { @@ -98,6 +99,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: + '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + + '\n' + + 'Here’s how to create a workspace:\n' + + '\n' + + '1. Click the settings tab.\n' + + '2. Click *Workspaces* > *New workspace*.\n' + + '\n' + + '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', +}; + +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 the settings tab.\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.', video: { @@ -5013,30 +5058,9 @@ const CONST = { height: 960, }, tasks: [ - { - type: 'createWorkspace', - autoCompleted: true, - title: 'Create a workspace', - description: - '*Create a workspace* to track expenses, scan receipts, chat, and more.\n' + - '\n' + - 'Here’s how to create a workspace:\n' + - '\n' + - '1. Click the settings tab.\n' + - '2. Click *Workspaces* > *New workspace*.\n' + - '\n' + - '*Your new workspace is ready! It’ll keep all of your spend (and chats) in one place.*', - }, + 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, @@ -5046,24 +5070,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 the settings tab.\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, @@ -5140,6 +5147,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.', diff --git a/src/libs/API/parameters/CreateWorkspaceParams.ts b/src/libs/API/parameters/CreateWorkspaceParams.ts index 91c1039169aa..1b8cfcaeecd5 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; }; export default CreateWorkspaceParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ac396709b07..fb5a1265fc5c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2707,7 +2707,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); @@ -3876,6 +3876,8 @@ function categorizeTrackedExpense(trackedExpenseParams: CategorizeTrackedExpense 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}); @@ -3957,6 +3959,8 @@ function shareTrackedExpense( 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}); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index f28a82bea9bb..a2b64cb8e8ba 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -5,6 +5,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {ReportExportType} from '@components/ButtonWithDropdownMenu/types'; +import {prepareOnboardingOnyxData} from '@libs/actions/Report'; import * as API from '@libs/API'; import type { AddBillingCardAndRequestWorkspaceOwnerChangeParams, @@ -78,9 +79,11 @@ import {getAllReportTransactions} from '@libs/TransactionUtils'; 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, @@ -1680,6 +1683,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 * @@ -1689,7 +1698,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string) { +function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: OnboardingPurpose) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(); @@ -1951,9 +1960,24 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName expenseCreatedReportActionID, customUnitID, customUnitRateID, - engagementChoice, }; + if (!introSelected?.createWorkspace && engagementChoice) { + const { + guidedSetupData, + optimisticData: taskOptimisticData, + successData: taskSuccessData, + failureData: taskFailureData, + } = prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], expenseChatReportID, policyID); + + params.guidedSetupData = JSON.stringify(guidedSetupData); + params.engagementChoice = engagementChoice; + + optimisticData.push(...taskOptimisticData); + successData.push(...taskSuccessData); + failureData.push(...taskFailureData); + } + return {successData, optimisticData, failureData, params}; } @@ -1966,7 +1990,13 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName * @param [policyID] custom policy id we will use for created workspace * @param [engagementChoice] Purpose of using application selected by user in guided setup flow */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = ''): CreateWorkspaceParams { +function createWorkspace( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + engagementChoice = CONST.ONBOARDING_CHOICES.MANAGE_TEAM, +): CreateWorkspaceParams { const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 783a2c80d10e..a77a35b65629 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -933,7 +933,7 @@ function openReport( onboardingMessage.tasks = updatedTasks; } - const onboardingData = prepareOnboardingOptimisticData(choice, onboardingMessage); + const onboardingData = prepareOnboardingOnyxData(choice, onboardingMessage); optimisticData.push(...onboardingData.optimisticData, { onyxMethod: Onyx.METHOD.MERGE, @@ -3593,7 +3593,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, @@ -3656,6 +3656,7 @@ function prepareOnboardingOptimisticData( reportComment: videoComment.commentText, }; } + let createWorkspaceTaskReportID; const tasksData = data.tasks .filter((task) => { if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) { @@ -3665,6 +3666,14 @@ 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].includes(introSelected?.choice as SkipViewTourOnboardingChoices) && + engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM + ) { + return false; + } return true; }) .map((task, index) => { @@ -3705,6 +3714,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, @@ -3881,7 +3893,6 @@ function prepareOnboardingOptimisticData( const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData]; const lastVisibleActionCreated = welcomeSignOffCommentAction.created; - optimisticData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -3896,7 +3907,10 @@ function prepareOnboardingOptimisticData( { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_INTRO_SELECTED, - value: {choice: engagementChoice}, + value: { + choice: engagementChoice, + createWorkspace: createWorkspaceTaskReportID, + }, }, ); @@ -3962,7 +3976,10 @@ 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 @@ -4125,33 +4142,35 @@ function prepareOnboardingOptimisticData( } } - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, - value: { - [welcomeSignOffCommentAction.reportActionID]: welcomeSignOffCommentAction as ReportAction, - }, - }); + if (!introSelected?.choice) { + 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}, - }, - }); + 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, - }, - }); + 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}); + } - guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage}); return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters}; } @@ -4168,7 +4187,7 @@ function completeOnboarding( userReportedIntegration?: OnboardingAccounting, wasInvited?: boolean, ) { - const {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters} = prepareOnboardingOptimisticData( + const {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters} = prepareOnboardingOnyxData( engagementChoice, data, adminsChatReportID, @@ -4718,4 +4737,5 @@ export { getConciergeReportID, setDeleteTransactionNavigateBackUrl, clearDeleteTransactionNavigateBackUrl, + prepareOnboardingOnyxData, }; 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; From 7014f2d25032b75d7c0305d0bdba4e6ab0124c8b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 15 Jan 2025 23:53:13 +0530 Subject: [PATCH 02/94] prettier --- src/libs/actions/Report.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a77a35b65629..14064b65722f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3666,14 +3666,16 @@ function prepareOnboardingOnyxData( if (['addAccountingIntegration', 'setupCategoriesAndTags'].includes(task.type) && !userReportedIntegration) { return false; } - type SkipViewTourOnboardingChoices = "newDotSubmit" | "newDotSplitChat" | "newDotPersonalSpend" | "newDotEmployer" + 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].includes(introSelected?.choice as SkipViewTourOnboardingChoices) && + [CONST.ONBOARDING_CHOICES.EMPLOYER, CONST.ONBOARDING_CHOICES.PERSONAL_SPEND, CONST.ONBOARDING_CHOICES.SUBMIT, CONST.ONBOARDING_CHOICES.CHAT_SPLIT].includes( + introSelected?.choice as SkipViewTourOnboardingChoices, + ) && engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ) { - return false; - } + return false; + } return true; }) .map((task, index) => { @@ -3715,7 +3717,7 @@ function prepareOnboardingOnyxData( ? buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, 'marked as complete', actorAccountID, 2) : null; if (task.type === 'createWorkspace') { - createWorkspaceTaskReportID = currentTask.reportID + createWorkspaceTaskReportID = currentTask.reportID; } return { @@ -4171,7 +4173,6 @@ function prepareOnboardingOnyxData( guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage}); } - return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters}; } From c9b2f4f5808482757af489611e5cf2e6a66c91d3 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 11 Jan 2025 02:19:37 +0530 Subject: [PATCH 03/94] replace meetSetupSpecialist task with meetGuideTask and fix guidedSetupData being empty --- src/CONST.ts | 10 +--------- src/libs/actions/Report.ts | 4 +++- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 22d5d0ab1cea..e656194d1d3e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5241,15 +5241,7 @@ const CONST = { height: 960, }, 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, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 14064b65722f..bf3587bd8e52 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -4144,6 +4144,8 @@ function prepareOnboardingOnyxData( } } + guidedSetupData.push(...tasksForParameters); + if (!introSelected?.choice) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -4170,7 +4172,7 @@ function prepareOnboardingOnyxData( } as ReportAction, }, }); - guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage}); + guidedSetupData.push({type: 'message', ...welcomeSignOffMessage}); } return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters}; From 61609ecd4ed3d09ffa2f2a4dd4a761d81b8eb0c1 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> Date: Wed, 15 Jan 2025 01:29:19 +0530 Subject: [PATCH 04/94] fix upgrade workspace case --- src/libs/actions/Policy/Policy.ts | 2 +- src/pages/iou/request/step/IOURequestStepUpgrade.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index a2b64cb8e8ba..c9c42903ab8a 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1995,7 +1995,7 @@ function createWorkspace( makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), - engagementChoice = CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + engagementChoice: OnboardingPurpose = CONST.ONBOARDING_CHOICES.MANAGE_TEAM, ): CreateWorkspaceParams { const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index 1abfb799a5fa..f38cbb96e3d5 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -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; }} From 49213321148696629b06f7bd9e71f71a34b4767e Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 16 Jan 2025 00:11:48 +0530 Subject: [PATCH 05/94] namespace imports lint failure --- src/pages/iou/request/step/IOURequestStepUpgrade.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index f38cbb96e3d5..a0132d8d60c5 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -10,7 +10,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import UpgradeConfirmation from '@pages/workspace/upgrade/UpgradeConfirmation'; import UpgradeIntro from '@pages/workspace/upgrade/UpgradeIntro'; -import * as IOU from '@userActions/IOU'; +import {setMoneyRequestParticipants} from '@userActions/IOU'; import CONST from '@src/CONST'; import * as Policy from '@src/libs/actions/Policy/Policy'; import ROUTES from '@src/ROUTES'; @@ -46,7 +46,7 @@ function IOURequestStepUpgrade({ {!!isUpgraded && ( { - IOU.setMoneyRequestParticipants(transactionID, [ + setMoneyRequestParticipants(transactionID, [ { selected: true, accountID: 0, From 1063cfa56d36921f8b696734ddec2bc05395d63f Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 16 Jan 2025 00:16:32 +0530 Subject: [PATCH 06/94] fix Do not default string IDs lint failure --- src/ROUTES.ts | 2 +- src/pages/iou/request/step/IOURequestStepUpgrade.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fa40bfad4e63..467253a147f7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -472,7 +472,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/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index a0132d8d60c5..05af485cd7cf 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -51,13 +51,13 @@ function IOURequestStepUpgrade({ 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 From 3bf6fda2752bd70590dca259882f668930650a8f Mon Sep 17 00:00:00 2001 From: Andrii Vitiv Date: Wed, 15 Jan 2025 20:25:14 +0200 Subject: [PATCH 07/94] Improve receipt file detection --- src/libs/HttpUtils.ts | 34 +---------------------- src/libs/actions/IOU.ts | 9 +++--- src/libs/isFileUploadable/index.native.ts | 10 +++++++ src/libs/isFileUploadable/index.ts | 7 +++++ src/libs/validateFormDataParameter.ts | 29 +++++++++++++++++++ 5 files changed, 52 insertions(+), 37 deletions(-) create mode 100644 src/libs/isFileUploadable/index.native.ts create mode 100644 src/libs/isFileUploadable/index.ts create mode 100644 src/libs/validateFormDataParameter.ts diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 1fa4c7b8b4b5..158a667468f0 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -10,10 +10,7 @@ import {alertUser} from './actions/UpdateRequired'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from './API/types'; import {getCommandURL} from './ApiUtils'; import HttpsError from './Errors/HttpsError'; -import getPlatform from './getPlatform'; - -const platform = getPlatform(); -const isNativePlatform = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS; +import validateFormDataParameter from './validateFormDataParameter'; let shouldFailAllRequests = false; let shouldForceOffline = false; @@ -178,35 +175,6 @@ function xhr(command: string, data: Record, type: RequestType = return processHTTPRequest(url, type, formData, abortSignalController?.signal); } -/** - * Ensures no value of type `object` other than null, Blob, its subclasses, or {uri: string} (native platforms only) is passed to XMLHttpRequest. - * Otherwise, it will be incorrectly serialized as `[object Object]` and cause an error on Android. - * See https://github.com/Expensify/App/issues/45086 - */ -function validateFormDataParameter(command: string, key: string, value: unknown) { - // eslint-disable-next-line @typescript-eslint/no-shadow - const isValid = (value: unknown, isTopLevel: boolean): boolean => { - if (value === null || typeof value !== 'object') { - return true; - } - if (Array.isArray(value)) { - 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 false; - }; - - if (!isValid(value, true)) { - // eslint-disable-next-line no-console - console.warn(`An unsupported value was passed to command '${command}' (parameter: '${key}'). Only Blob and primitive types are allowed.`); - } -} - function cancelPendingRequests(command: AbortCommand = ABORT_COMMANDS.All) { const controller = abortControllerMap.get(command); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ac396709b07..8f6f899bd7c7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -43,6 +43,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'; @@ -3952,7 +3953,7 @@ function shareTrackedExpense( taxCode, taxAmount, billable, - receipt: receipt instanceof Blob ? receipt : undefined, + receipt: isFileUploadable(receipt) ? receipt : undefined, policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID, policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID, adminsChatReportID: createdWorkspaceParams?.adminsChatReportID, @@ -4064,7 +4065,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, @@ -4255,7 +4256,7 @@ function trackExpense( category, tag, billable, - receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined, + receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined, }; const policyParams: CategorizeTrackedExpensePolicyParams = { policyID: chatReport?.policyID ?? '-1', @@ -4327,7 +4328,7 @@ function trackExpense( createdChatReportActionID: createdChatReportActionID ?? '-1', 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/isFileUploadable/index.native.ts b/src/libs/isFileUploadable/index.native.ts new file mode 100644 index 000000000000..9b0fa9bec795 --- /dev/null +++ b/src/libs/isFileUploadable/index.native.ts @@ -0,0 +1,10 @@ +import type {FileObject} from '@components/AttachmentModal'; + +function isFileUploadable(file: 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..48c156313daa --- /dev/null +++ b/src/libs/isFileUploadable/index.ts @@ -0,0 +1,7 @@ +import type {FileObject} from '@components/AttachmentModal'; + +function isFileUploadable(file: File | FileObject | undefined): boolean { + return file instanceof Blob; +} + +export default isFileUploadable; diff --git a/src/libs/validateFormDataParameter.ts b/src/libs/validateFormDataParameter.ts new file mode 100644 index 000000000000..dd46c97a7941 --- /dev/null +++ b/src/libs/validateFormDataParameter.ts @@ -0,0 +1,29 @@ +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. + * Otherwise, it will be incorrectly serialized as `[object Object]` and cause an error on Android. + * See https://github.com/Expensify/App/issues/45086 + */ +function validateFormDataParameter(command: string, key: string, value: unknown) { + // eslint-disable-next-line @typescript-eslint/no-shadow + const isValid = (value: unknown, isTopLevel: boolean): boolean => { + if (value === null || typeof value !== 'object') { + return true; + } + if (Array.isArray(value)) { + return value.every((element) => isValid(element, false)); + } + if (isTopLevel) { + return isFileUploadable(value); + } + return false; + }; + + if (!isValid(value, true)) { + // eslint-disable-next-line no-console + console.warn(`An unsupported value was passed to command '${command}' (parameter: '${key}'). Only Blob and primitive types are allowed.`); + } +} + +export default validateFormDataParameter; From c446d64bfaec0073fb58d978fcfd86c76bde38e9 Mon Sep 17 00:00:00 2001 From: Amoralchik Date: Thu, 16 Jan 2025 16:21:43 +0200 Subject: [PATCH 08/94] Add custom emoji functionality and for onboarding task --- src/CONST.ts | 14 +- src/components/FloatingActionButton.tsx | 72 ++++-- .../BaseHTMLEngineProvider.tsx | 1 + .../HTMLRenderers/CustomEmojiRenderer.tsx | 13 + .../HTMLEngineProvider/HTMLRenderers/index.ts | 2 + src/libs/ReportUtils.ts | 18 +- src/libs/actions/Report.ts | 1 + .../FloatingActionButtonAndPopover.tsx | 230 ++++++++++-------- .../SidebarScreen/FloatingActionEmoji.tsx | 8 + src/styles/index.ts | 9 + 10 files changed, 227 insertions(+), 141 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx create mode 100644 src/pages/home/sidebar/SidebarScreen/FloatingActionEmoji.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 9fde47bca7bf..e1e39fa1dff4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -121,7 +121,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' + @@ -144,7 +144,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' + @@ -175,7 +175,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 *Track expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click *Track*.\n' + @@ -197,7 +197,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' + @@ -5127,7 +5127,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' + @@ -5144,7 +5144,7 @@ const CONST = { '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Hit 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, hit the *+* button on the message bar, and hit *Split expense*.\n' + @@ -5194,7 +5194,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' + diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 04bc4847a00f..aab270ae71b1 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -58,9 +58,12 @@ type FloatingActionButtonProps = { /* An accessibility role for the button */ role: Role; + + /* An accessibility render as emoji for the button */ + isEmoji?: boolean; }; -function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: FloatingActionButtonProps, ref: ForwardedRef) { +function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isEmoji}: FloatingActionButtonProps, ref: ForwardedRef) { const {success, buttonDefaultBG, textLight, textDark} = useTheme(); const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; @@ -117,6 +120,46 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo onPress(event); }; + const floatingActionStyle = isEmoji ? styles.floatingActionEmoji : styles.floatingActionButton; + const iconSizeStyle = isEmoji ? variables.iconSizeExtraSmall : variables.iconSizeNormal; + const nativePositionFix = Platform.OS === 'web' ? [] : {height: '6%'}; + const pressebleStyles = isEmoji ? [nativePositionFix] : [styles.h100, styles.bottomTabBarItem]; + const animatedStyles = [floatingActionStyle, isEmoji ? {} : animatedStyle]; + + const button = ( + { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } + }} + style={pressebleStyles} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} + > + + + + + + + ); + + if (isEmoji) { + return button; + } + return ( - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } - }} - style={[styles.h100, styles.bottomTabBarItem]} - accessibilityLabel={accessibilityLabel} - onPress={toggleFabAction} - onLongPress={() => {}} - role={role} - shouldUseHapticsOnLongPress={false} - > - - - - - - + {button} ); } diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index b4002767524f..b7b0572181b6 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -75,6 +75,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim 'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}), 'mention-report': HTMLElementModel.fromCustomModel({tagName: 'mention-report', contentModel: HTMLContentModel.textual}), 'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}), + '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/HTMLRenderers/CustomEmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx new file mode 100644 index 000000000000..5a739060a160 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import FloatingActionEmoji from '@pages/home/sidebar/SidebarScreen/FloatingActionEmoji'; + +function CustomEmojiRenderer({tnode}: CustomRendererProps) { + if (tnode.attributes.emoji === 'action-menu-icon') { + return ; + } + + return ''; +} + +export default CustomEmojiRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index ce24584048b0..4d589591c03f 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 EditedRenderer from './EditedRenderer'; import EmojiRenderer from './EmojiRenderer'; import ImageRenderer from './ImageRenderer'; @@ -28,6 +29,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'mention-user': MentionUserRenderer, 'mention-report': MentionReportRenderer, 'mention-here': MentionHereRenderer, + 'custom-emoji': CustomEmojiRenderer, emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 57951ece5c21..7a5f12317634 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4522,7 +4522,7 @@ function completeShortMention(text: string): string { * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! */ -function getParsedComment(text: string, parsingDetails?: ParsingDetails): string { +function getParsedComment(text: string, parsingDetails?: ParsingDetails, isAllowCustomEmoji?: boolean): string { let isGroupPolicyReport = false; if (parsingDetails?.reportID) { const currentReport = getReportOrDraftReport(parsingDetails?.reportID); @@ -4538,9 +4538,16 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string const textWithMention = completeShortMention(text); - return text.length <= CONST.MAX_MARKUP_LENGTH - ? Parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) - : lodashEscape(text); + let result = + text.length <= CONST.MAX_MARKUP_LENGTH + ? Parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) + : lodashEscape(text); + + if (isAllowCustomEmoji) { + result = result.replace(/<custom-emoji emoji="/g, ''); + } + + return result; } function getUploadingAttachmentHtml(file?: FileObject): string { @@ -6342,6 +6349,7 @@ function buildOptimisticTaskReport( description?: string, policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + isAllowCustomEmoji?: boolean, ): OptimisticTaskReport { const participants: Participants = { [ownerAccountID]: { @@ -6356,7 +6364,7 @@ function buildOptimisticTaskReport( return { reportID: generateReportID(), reportName: title, - description: getParsedComment(description ?? ''), + description: getParsedComment(description ?? '', undefined, isAllowCustomEmoji), ownerAccountID, participants, managerID: assigneeAccountID, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 709d160a6f08..a1b3f2fadcf9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3695,6 +3695,7 @@ function prepareOnboardingOptimisticData( taskDescription, targetChatPolicyID, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + true, // is Allow render custom emoji ); const emailCreatingAction = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? allPersonalDetails?.[actorAccountID]?.login ?? CONST.EMAIL.CONCIERGE : CONST.EMAIL.CONCIERGE; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 5c082e3d50f7..1a9c0cfaa788 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -68,6 +68,9 @@ type FloatingActionButtonAndPopoverProps = { /* Callback function before the menu is hidden */ onHideCreateMenu?: () => void; + + /* An accessibility render as emoji for the button */ + isEmoji?: boolean; }; type FloatingActionButtonAndPopoverRef = { @@ -167,7 +170,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}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -452,109 +455,132 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl const canModifyTask = Task.canModifyTask(viewTourTaskReport, currentUserPersonalDetails.accountID); const canActionTask = Task.canActionTask(viewTourTaskReport, currentUserPersonalDetails.accountID); - return ( - - interceptAnonymousUser(Report.startNewChat), - }, - ...(canSendInvoice - ? [ - { - icon: Expensicons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); - return; - } - - IOU.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. - ReportUtils.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: () => { - Link.openExternalLink(navatticURL); - Welcome.setSelfTourViewed(Session.isAnonymousUser()); - if (viewTourTaskReport && canModifyTask && canActionTask) { - Task.completeTask(viewTourTaskReport); + const popoverMenu = ( + interceptAnonymousUser(Report.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(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()), + + IOU.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. + ReportUtils.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: () => { + Link.openExternalLink(navatticURL); + Welcome.setSelfTourViewed(Session.isAnonymousUser()); + if (viewTourTaskReport && canModifyTask && canActionTask) { + Task.completeTask(viewTourTaskReport); + } }, - ] - : []), - ...quickActionMenuItems, - ]} - withoutOverlay - anchorRef={fabRef} - /> - { - setModalVisible(false); - Link.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(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()), + }, + ] + : []), + ...quickActionMenuItems, + ]} + withoutOverlay + anchorRef={fabRef} + /> + ); + const confirmModal = ( + { + setModalVisible(false); + Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX); + }} + onCancel={() => setModalVisible(false)} + title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')} + confirmText={translate('exitSurvey.goToExpensifyClassic')} + cancelText={translate('common.cancel')} + /> + ); + const floatingActionButton = ( + + ); + + if (isEmoji) { + return ( + <> + + {popoverMenu} + {confirmModal} + + {floatingActionButton} + + ); + } + + return ( + + {popoverMenu} + {confirmModal} + {floatingActionButton} ); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionEmoji.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionEmoji.tsx new file mode 100644 index 000000000000..fa5217acb083 --- /dev/null +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionEmoji.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import FloatingActionButtonAndPopover from './FloatingActionButtonAndPopover'; + +function FloatingActionEmoji() { + return ; +} + +export default FloatingActionEmoji; diff --git a/src/styles/index.ts b/src/styles/index.ts index 06feb42b3fe2..6857c727fad1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1658,6 +1658,15 @@ const styles = (theme: ThemeColors) => justifyContent: 'center', }, + floatingActionEmoji: { + backgroundColor: theme.success, + height: variables.iconSizeSmall, + width: variables.iconSizeSmall, + borderRadius: 999, + alignItems: 'center', + justifyContent: 'center', + }, + sidebarFooterUsername: { color: theme.heading, fontSize: variables.fontSizeLabel, From 85c73bbd90ddd3167ebdb68b1b8b41ae0c867b38 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 17 Jan 2025 03:13:15 +0530 Subject: [PATCH 09/94] update onboarding task posting logic for TRACK_WORKSPACE action --- src/libs/actions/Report.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bf3587bd8e52..49fd941b3873 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3614,8 +3614,11 @@ function prepareOnboardingOnyxData( } } - // 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 integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : ''; const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`]; const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]); From d83382d3f94ceb424121a226e794bc7774835cbd Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 17 Jan 2025 03:35:51 +0530 Subject: [PATCH 10/94] workaround for failing lint --- src/libs/ReportUtils.ts | 3 ++- src/libs/actions/Policy/Policy.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3115d73997b8..d8e476a887de 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} from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -9138,6 +9138,7 @@ export { getReportMetadata, buildOptimisticSelfDMReport, isHiddenForCurrentUser, + prepareOnboardingOnyxData, }; export type { diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index c9c42903ab8a..0403a420ce09 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -5,7 +5,6 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {ReportExportType} from '@components/ButtonWithDropdownMenu/types'; -import {prepareOnboardingOnyxData} from '@libs/actions/Report'; import * as API from '@libs/API'; import type { AddBillingCardAndRequestWorkspaceOwnerChangeParams, @@ -1968,7 +1967,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName optimisticData: taskOptimisticData, successData: taskSuccessData, failureData: taskFailureData, - } = prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], expenseChatReportID, policyID); + } = ReportUtils.prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], expenseChatReportID, policyID); params.guidedSetupData = JSON.stringify(guidedSetupData); params.engagementChoice = engagementChoice; From 06e71bca6ec48e96268ecb8f7fd9ea2c67761958 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 17 Jan 2025 03:50:03 +0530 Subject: [PATCH 11/94] fix: update to correct adminsChatReportID param --- src/libs/actions/Policy/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 0403a420ce09..7fc98701237f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1967,7 +1967,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName optimisticData: taskOptimisticData, successData: taskSuccessData, failureData: taskFailureData, - } = ReportUtils.prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], expenseChatReportID, policyID); + } = ReportUtils.prepareOnboardingOnyxData(engagementChoice, CONST.ONBOARDING_MESSAGES[engagementChoice], adminsChatReportID, policyID); params.guidedSetupData = JSON.stringify(guidedSetupData); params.engagementChoice = engagementChoice; From 238cfa80263289850fa7cdda2f5376c2ff73ab51 Mon Sep 17 00:00:00 2001 From: Amoralchik Date: Fri, 17 Jan 2025 13:11:23 +0200 Subject: [PATCH 12/94] Add GlobalCreate svg and update FloatingActionButton to use it --- assets/images/customEmoji/global-create.svg | 14 ++++ src/components/FloatingActionButton.tsx | 87 ++++++++++++--------- src/styles/index.ts | 9 --- 3 files changed, 63 insertions(+), 47 deletions(-) create mode 100644 assets/images/customEmoji/global-create.svg 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/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index aab270ae71b1..ee29a37a0674 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -6,6 +6,7 @@ import {Platform} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; +import GlobalCreateIcon from '@assets/images/customEmoji/global-create.svg'; import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; import useIsCurrentRouteHome from '@hooks/useIsCurrentRouteHome'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -15,6 +16,7 @@ import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ImageSVG from './ImageSVG'; import {PressableWithoutFeedback} from './Pressable'; import {useProductTrainingContext} from './ProductTrainingContext'; import EducationalTooltip from './Tooltip/EducationalTooltip'; @@ -120,44 +122,28 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isEm onPress(event); }; - const floatingActionStyle = isEmoji ? styles.floatingActionEmoji : styles.floatingActionButton; - const iconSizeStyle = isEmoji ? variables.iconSizeExtraSmall : variables.iconSizeNormal; - const nativePositionFix = Platform.OS === 'web' ? [] : {height: '6%'}; - const pressebleStyles = isEmoji ? [nativePositionFix] : [styles.h100, styles.bottomTabBarItem]; - const animatedStyles = [floatingActionStyle, isEmoji ? {} : animatedStyle]; - - const button = ( - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } - }} - style={pressebleStyles} - accessibilityLabel={accessibilityLabel} - onPress={toggleFabAction} - onLongPress={() => {}} - role={role} - shouldUseHapticsOnLongPress={false} - > - - - - - - - ); - if (isEmoji) { - return button; + return ( + { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } + }} + style={{verticalAlign: 'bottom'}} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} + > + + + ); } return ( @@ -172,7 +158,32 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role, isEm wrapperStyle={styles.productTrainingTooltipWrapper} shouldHideOnNavigate={false} > - {button} + { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } + }} + style={[styles.h100, styles.bottomTabBarItem]} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} + > + + + + + + ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index 6857c727fad1..06feb42b3fe2 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1658,15 +1658,6 @@ const styles = (theme: ThemeColors) => justifyContent: 'center', }, - floatingActionEmoji: { - backgroundColor: theme.success, - height: variables.iconSizeSmall, - width: variables.iconSizeSmall, - borderRadius: 999, - alignItems: 'center', - justifyContent: 'center', - }, - sidebarFooterUsername: { color: theme.heading, fontSize: variables.fontSizeLabel, From b3150403361924bb838e86b1892794fb63d8782a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 20 Jan 2025 02:25:13 +0530 Subject: [PATCH 13/94] fi onboarding accounting page not showing --- src/libs/actions/Policy/Policy.ts | 9 ++++++--- .../OnboardingAccounting/BaseOnboardingAccounting.tsx | 2 +- .../OnboardingEmployees/BaseOnboardingEmployees.tsx | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 7fc98701237f..c5bf47c9f64f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1696,8 +1696,10 @@ Onyx.connect({ * @param [policyName] custom policy name we will use for created workspace * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace + * @param [engagementChoice] the engagement choice for the workspace + * @param [shouldAddOnboardingTasks] whether to add onboarding tasks to the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: OnboardingPurpose) { + function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: OnboardingPurpose, shouldAddOnboardingTasks = true) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(); @@ -1961,7 +1963,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName customUnitRateID, }; - if (!introSelected?.createWorkspace && engagementChoice) { + if (!introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) { const { guidedSetupData, optimisticData: taskOptimisticData, @@ -1995,8 +1997,9 @@ function createWorkspace( policyName = '', policyID = generatePolicyID(), engagementChoice: OnboardingPurpose = CONST.ONBOARDING_CHOICES.MANAGE_TEAM, + shouldAddOnboardingTasks = true, ): CreateWorkspaceParams { - const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, 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/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index a14f30216051..4855cd415276 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -67,7 +67,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount return; } - const {adminsChatReportID, policyID} = Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM); + const {adminsChatReportID, policyID} = Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, false); Welcome.setOnboardingAdminsChatReportID(adminsChatReportID); Welcome.setOnboardingPolicyID(policyID); }, [isVsb, paidGroupPolicy, allPolicies, allPoliciesResult]); diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx index 12722e87f05a..f31fb939a522 100644 --- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx +++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx @@ -76,7 +76,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE // We need `adminsChatReportID` for `Report.completeOnboarding`, but at the same time, we don't want to call `Policy.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) + ? Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM, false) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (shouldCreateWorkspace) { From bb02e14242be76066c9899b6541335c178455868 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 20 Jan 2025 05:07:24 +0530 Subject: [PATCH 14/94] fix: post tasks in admin room --- src/libs/actions/Policy/Policy.ts | 12 +++++++++-- src/libs/actions/Report.ts | 34 ++++++++++++++++++------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index c5bf47c9f64f..4868851908e8 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1699,7 +1699,15 @@ Onyx.connect({ * @param [engagementChoice] the engagement choice for the workspace * @param [shouldAddOnboardingTasks] whether to add onboarding tasks to the workspace */ - function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: OnboardingPurpose, shouldAddOnboardingTasks = true) { +function buildPolicyData( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + expenseReportId?: string, + engagementChoice?: OnboardingPurpose, + shouldAddOnboardingTasks = true, +) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(); @@ -1963,7 +1971,7 @@ Onyx.connect({ customUnitRateID, }; - if (!introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) { + if (engagementChoice && shouldAddOnboardingTasks) { const { guidedSetupData, optimisticData: taskOptimisticData, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 232b54f3191f..4094f3192231 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3618,12 +3618,14 @@ function prepareOnboardingOnyxData( !currentUserEmail?.includes('+'); const integrationName = userReportedIntegration ? CONST.ONBOARDING_ACCOUNTING_MAPPING[userReportedIntegration] : ''; const adminsChatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`]; - const targetChatReport = shouldPostTasksInAdminsRoom ? adminsChatReport : getChatByParticipants([CONST.ACCOUNT_ID.CONCIERGE, currentUserAccountID]); + const targetChatReport = shouldPostTasksInAdminsRoom + ? adminsChatReport ?? {reportID: adminsChatReportID, policyID: onboardingPolicyID} + : 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) { + if (assignedGuidePersonalDetail && assignedGuidePersonalDetail.accountID) { assignedGuideAccountID = assignedGuidePersonalDetail.accountID; } else { assignedGuideAccountID = generateAccountID(assignedGuideEmail); @@ -3669,9 +3671,13 @@ function prepareOnboardingOnyxData( 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].includes( - introSelected?.choice as SkipViewTourOnboardingChoices, - ) && + [ + 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; @@ -3916,8 +3922,8 @@ function prepareOnboardingOnyxData( }, ); - // 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}`, @@ -3937,8 +3943,8 @@ function prepareOnboardingOnyxData( 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}`, @@ -3984,8 +3990,8 @@ function prepareOnboardingOnyxData( }, }, ); - // 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}`, @@ -4037,10 +4043,10 @@ function prepareOnboardingOnyxData( }); } - // 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 = shouldPostTasksInAdminsRoom && !!introSelected?.choice ? [] : [{type: 'message', ...textMessage}]; - if (!shouldPostTasksInAdminsRoom && 'video' in data && data.video && videoCommentAction && videoMessage) { + if ((!shouldPostTasksInAdminsRoom || !!introSelected?.choice) && 'video' in data && data.video && videoCommentAction && videoMessage) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetChatReportID}`, From eb483d1b8b54a4ed7dc500717c7aeec38a3a4fea Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Mon, 20 Jan 2025 15:43:52 +0530 Subject: [PATCH 15/94] reverse uninteded change --- src/libs/actions/Policy/Policy.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 4868851908e8..c5bf47c9f64f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1699,15 +1699,7 @@ Onyx.connect({ * @param [engagementChoice] the engagement choice for the workspace * @param [shouldAddOnboardingTasks] whether to add onboarding tasks to the workspace */ -function buildPolicyData( - policyOwnerEmail = '', - makeMeAdmin = false, - policyName = '', - policyID = generatePolicyID(), - expenseReportId?: string, - engagementChoice?: OnboardingPurpose, - shouldAddOnboardingTasks = true, -) { + function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: OnboardingPurpose, shouldAddOnboardingTasks = true) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticDistanceRateCustomUnits(); @@ -1971,7 +1963,7 @@ function buildPolicyData( customUnitRateID, }; - if (engagementChoice && shouldAddOnboardingTasks) { + if (!introSelected?.createWorkspace && engagementChoice && shouldAddOnboardingTasks) { const { guidedSetupData, optimisticData: taskOptimisticData, From 5cc68ab5a77680d559b1070d629b4d077c0d0874 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 20 Jan 2025 12:16:40 +0100 Subject: [PATCH 16/94] feat: add settlement date info --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/params.ts | 5 +++++ .../expensifyCard/WorkspaceCardListHeader.tsx | 11 +++++++---- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f1c53a6b91a1..f0a5ca4172f8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -141,6 +141,7 @@ import type { SetTheRequestParams, SettledAfterAddedBankAccountParams, SettleExpensifyCardParams, + SettlementDateParams, ShareParams, SignUpNewFaceCodeParams, SizeExceededParams, @@ -3456,6 +3457,7 @@ 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}`, cardLimit: 'Card limit', remainingLimit: 'Remaining limit', requestLimitIncrease: 'Request limit increase', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6b70e4876753..cccd85c70438 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -140,6 +140,7 @@ import type { SetTheRequestParams, SettledAfterAddedBankAccountParams, SettleExpensifyCardParams, + SettlementDateParams, ShareParams, SignUpNewFaceCodeParams, SizeExceededParams, @@ -3497,6 +3498,7 @@ 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}.`, 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 f9ca26a3575a..d40e27d7dec0 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -594,6 +594,10 @@ type FlightLayoverParams = { layover: string; }; +type SettlementDateParams = { + settlementDate: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -804,4 +808,5 @@ export type { ChatWithAccountManagerParams, EditDestinationSubtitleParams, FlightLayoverParams, + SettlementDateParams, }; diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx index e9aac5685dc1..325f93428400 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx @@ -30,10 +30,13 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) { - + + + {translate('workspace.expensifyCard.balanceWillBeSettledOn', {settlementDate: 'date'})} + Date: Mon, 20 Jan 2025 16:03:26 +0100 Subject: [PATCH 17/94] feat: add translations and settle balance button --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../expensifyCard/WorkspaceCardListHeader.tsx | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f0a5ca4172f8..a68a2102b3fb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3458,6 +3458,7 @@ const translations = { 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 cccd85c70438..4dc6d22df0b2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3499,6 +3499,7 @@ const translations = { 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/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx index 325f93428400..9bb711699d2a 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardListHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -31,10 +32,19 @@ function WorkspaceCardListHeader({policyID}: WorkspaceCardListHeaderProps) { - + + + +