From ba863a37efd5d724d51af8b41133978ed54c915c Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 9 Jan 2025 22:32:23 +0530 Subject: [PATCH 01/98] 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 789e9b588609..687112baacb1 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: { @@ -5014,30 +5059,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, @@ -5047,24 +5071,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, @@ -5141,6 +5148,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 ee49b1c8e803..03788dfb5469 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2642,7 +2642,7 @@ function getTrackExpenseInformation( let createdWorkspaceParams: CreateWorkspaceParams | undefined; if (isDraftReport) { - const workspaceData = Policy.buildPolicyData(undefined, policy?.makeMeAdmin, policy?.name, policy?.id, chatReport?.reportID); + const workspaceData = Policy.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); @@ -3821,6 +3821,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}); @@ -3902,6 +3904,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 c04648c04104..278114d171d3 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 * as TransactionUtils 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, @@ -1676,6 +1679,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 * @@ -1685,7 +1694,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(); @@ -1947,9 +1956,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}; } @@ -1962,7 +1986,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 1e157b983483..27dd0d850b4c 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -892,7 +892,7 @@ function openReport( onboardingMessage.tasks = updatedTasks; } - const onboardingData = prepareOnboardingOptimisticData(choice, onboardingMessage); + const onboardingData = prepareOnboardingOnyxData(choice, onboardingMessage); optimisticData.push(...onboardingData.optimisticData, { onyxMethod: Onyx.METHOD.MERGE, @@ -3555,7 +3555,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, @@ -3618,6 +3618,7 @@ function prepareOnboardingOptimisticData( reportComment: videoComment.commentText, }; } + let createWorkspaceTaskReportID; const tasksData = data.tasks .filter((task) => { if (['setupCategories', 'setupTags'].includes(task.type) && userReportedIntegration) { @@ -3627,6 +3628,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) => { @@ -3675,6 +3684,9 @@ function prepareOnboardingOptimisticData( const completedTaskReportAction = task.autoCompleted ? ReportUtils.buildOptimisticTaskReportAction(currentTask.reportID, CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED, 'marked as complete', actorAccountID, 2) : null; + if (task.type === 'createWorkspace') { + createWorkspaceTaskReportID = currentTask.reportID + } return { task, @@ -3856,7 +3868,6 @@ function prepareOnboardingOptimisticData( const optimisticData: OnyxUpdate[] = [...tasksForOptimisticData]; const lastVisibleActionCreated = welcomeSignOffCommentAction.created; - optimisticData.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -3871,7 +3882,10 @@ function prepareOnboardingOptimisticData( { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_INTRO_SELECTED, - value: {choice: engagementChoice}, + value: { + choice: engagementChoice, + createWorkspace: createWorkspaceTaskReportID, + }, }, ); @@ -3937,7 +3951,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 @@ -4100,33 +4117,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}; } @@ -4143,7 +4162,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, @@ -4693,4 +4712,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 32dc80935b1fa46995b1d8adea1e79b05776c36a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 10 Jan 2025 02:22:17 +0530 Subject: [PATCH 02/98] 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 27dd0d850b4c..6ac05869a09e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3628,14 +3628,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) => { @@ -3685,7 +3687,7 @@ function prepareOnboardingOnyxData( ? ReportUtils.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 { @@ -4146,7 +4148,6 @@ function prepareOnboardingOnyxData( guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage}); } - return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters}; } From ace0d1e01ee846f07a25f751703563b73e2e4269 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 11 Jan 2025 02:19:37 +0530 Subject: [PATCH 03/98] 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 111488022626..29a81afe7b17 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5243,15 +5243,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 6ac05869a09e..ac22b0b9d2e2 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -4119,6 +4119,8 @@ function prepareOnboardingOnyxData( } } + guidedSetupData.push(...tasksForParameters); + if (!introSelected?.choice) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -4145,7 +4147,7 @@ function prepareOnboardingOnyxData( } as ReportAction, }, }); - guidedSetupData.push(...tasksForParameters, {type: 'message', ...welcomeSignOffMessage}); + guidedSetupData.push({type: 'message', ...welcomeSignOffMessage}); } return {optimisticData, successData, failureData, guidedSetupData, actorAccountID, selfDMParameters}; From 46eaa10888c8ad0f9ee2ff35bb67eabcd95d7951 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/98] 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 6926f45a6b6145d8dae12d00a5f8dae746c79b43 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 14 Jan 2025 17:07:01 +0700 Subject: [PATCH 05/98] feat: Update the invitation process to include the role --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Policy/Member.ts | 6 ++-- .../workspace/WorkspaceInviteMessagePage.tsx | 34 +++++++++++++++++-- src/types/form/WorkspaceInviteMessageForm.ts | 2 ++ 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e976ae1f67db..d7abc856c262 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4178,6 +4178,7 @@ const translations = { }, inviteMessage: { inviteMessageTitle: 'Add message', + confirmDetails: 'Confirm details', inviteMessagePrompt: 'Make your invitation extra special by adding a message below!', personalMessagePrompt: 'Message', genericFailureMessage: 'An error occurred while inviting the member to the workspace. Please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index ae32a90bbcec..3e0faa34eca6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4223,6 +4223,7 @@ const translations = { }, inviteMessage: { inviteMessageTitle: 'Añadir un mensaje', + confirmDetails: 'Confirma los detalles', inviteMessagePrompt: '¡Añadir un mensaje para hacer tu invitación destacar!', personalMessagePrompt: 'Mensaje', inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar.', diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index da910af2c8ef..44b0a71dc72c 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -667,7 +667,7 @@ function clearWorkspaceOwnerChangeFlow(policyID: string) { * Adds members to the specified workspace/policyID * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ -function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string, policyMemberAccountIDs: number[]) { +function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string, policyMemberAccountIDs: number[], role: string) { const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const; const logins = Object.keys(invitedEmailsToAccountIDs).map((memberLogin) => PhoneNumber.addSMSDomainIfPhoneNumber(memberLogin)); const accountIDs = Object.values(invitedEmailsToAccountIDs); @@ -689,7 +689,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount optimisticMembersState[email] = { email, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - role: CONST.POLICY.ROLE.USER, + role, submitsTo: getDefaultApprover(allPolicies?.[policyKey]), }; successMembersState[email] = {pendingAction: null}; @@ -737,7 +737,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount failureData.push(...membersChats.onyxFailureData, ...announceRoomChat.onyxFailureData, ...announceRoomMembers.onyxFailureData); const params: AddMembersToWorkspaceParams = { - employees: JSON.stringify(logins.map((login) => ({email: login}))), + employees: JSON.stringify(logins.map((login) => ({email: login, role}))), ...(optimisticAnnounceChat.announceChatReportID ? {announceChatReportID: optimisticAnnounceChat.announceChatReportID} : {}), ...(optimisticAnnounceChat.announceChatReportActionID ? {announceCreatedReportActionID: optimisticAnnounceChat.announceChatReportActionID} : {}), welcomeNote: Parser.replace(welcomeNote, { diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 463b7df2124f..2464c69e5d6d 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {ValueOf} from 'type-fest'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors} from '@components/Form/types'; @@ -12,6 +13,7 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import ValuePicker from '@components/ValuePicker'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; @@ -49,6 +51,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const styles = useThemeStyles(); const {translate} = useLocalize(); const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT); + const [role, setRole] = useState>(CONST.POLICY.ROLE.USER); const viewportOffsetTop = useViewportOffsetTop(); const [welcomeNote, setWelcomeNote] = useState(); @@ -100,7 +103,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs); + Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs, role); Policy.setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); if ((route.params?.backTo as string)?.endsWith('members')) { @@ -127,6 +130,23 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const policyName = policy?.name; + const roleItems = useMemo(() => { + return Object.values(CONST.POLICY.ROLE).map((roleValue) => { + let label = ''; + if (roleValue === CONST.POLICY.ROLE.USER) { + label = translate('common.member'); + } else if (roleValue === CONST.POLICY.ROLE.ADMIN) { + label = translate('common.admin'); + } else { + label = translate('common.auditor'); + } + return { + label, + value: roleValue, + }; + }); + }, [translate]); + return ( {translate('workspace.inviteMessage.inviteMessagePrompt')} + + setRole(value as typeof role)} + /> + ; @@ -11,6 +12,7 @@ type WorkspaceInviteMessageForm = Form< InputID, { [INPUT_IDS.WELCOME_MESSAGE]: string; + [INPUT_IDS.ROLE]: string; } >; From d0b1cc17db50ff8f8e18c9e02a3a2ca965a18cad Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 14 Jan 2025 17:58:05 +0700 Subject: [PATCH 06/98] fix: add marginBottom 24px to role --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 2464c69e5d6d..b9d2771ef888 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -206,7 +206,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: {translate('workspace.inviteMessage.inviteMessagePrompt')} - + Date: Tue, 14 Jan 2025 18:09:41 +0700 Subject: [PATCH 07/98] fix: type --- tests/actions/PolicyMemberTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/PolicyMemberTest.ts b/tests/actions/PolicyMemberTest.ts index 6efbeece1cdb..41d9a72f1e5f 100644 --- a/tests/actions/PolicyMemberTest.ts +++ b/tests/actions/PolicyMemberTest.ts @@ -241,7 +241,7 @@ describe('actions/PolicyMember', () => { }); mockFetch?.pause?.(); - Member.addMembersToWorkspace({[newUserEmail]: 1234}, 'Welcome', policyID, []); + Member.addMembersToWorkspace({[newUserEmail]: 1234}, 'Welcome', policyID, [], CONST.POLICY.ROLE.USER); await waitForBatchedUpdates(); From a253588de8ca27f84258e00e0dd07812e0f13d69 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 15 Jan 2025 01:26:04 +0700 Subject: [PATCH 08/98] fix: update margin bottom role to mb3 --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index b9d2771ef888..4db5b9e2f446 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -206,7 +206,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: {translate('workspace.inviteMessage.inviteMessagePrompt')} - + Date: Wed, 15 Jan 2025 01:31:40 +0700 Subject: [PATCH 09/98] fix: lint --- .../workspace/WorkspaceInviteMessagePage.tsx | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 4db5b9e2f446..b9a91fa2ce32 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -20,18 +20,18 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; -import * as FormActions from '@libs/actions/FormActions'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import {openExternalLink} from '@libs/actions/Link'; +import {addMembersToWorkspace} from '@libs/actions/Policy/Member'; +import {setWorkspaceInviteMessageDraft} from '@libs/actions/Policy/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {SettingsNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; -import * as Link from '@userActions/Link'; -import * as Member from '@userActions/Policy/Member'; -import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -101,11 +101,11 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const sendInvitation = () => { Keyboard.dismiss(); - const policyMemberAccountIDs = Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs, role); - Policy.setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); - FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); + addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs, role); + setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); + clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); if ((route.params?.backTo as string)?.endsWith('members')) { Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal()); @@ -117,7 +117,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: /** Opens privacy url as an external link */ const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { event?.preventDefault(); - Link.openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); + openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); }; const validate = (): FormInputErrors => { @@ -151,7 +151,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: Date: Wed, 15 Jan 2025 04:15:57 +0700 Subject: [PATCH 10/98] fix: remove trans inviteMessageTitle --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d7abc856c262..3a8bcb0c18d5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4177,7 +4177,6 @@ const translations = { from: 'from', }, inviteMessage: { - inviteMessageTitle: 'Add message', confirmDetails: 'Confirm details', inviteMessagePrompt: 'Make your invitation extra special by adding a message below!', personalMessagePrompt: 'Message', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3e0faa34eca6..325a5bb59edc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4222,7 +4222,6 @@ const translations = { from: 'de', }, inviteMessage: { - inviteMessageTitle: 'Añadir un mensaje', confirmDetails: 'Confirma los detalles', inviteMessagePrompt: '¡Añadir un mensaje para hacer tu invitación destacar!', personalMessagePrompt: 'Mensaje', From d93f793815f573c6e645db45492cdf0e6fa8379e Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 14 Jan 2025 21:47:50 +0000 Subject: [PATCH 11/98] Update version to 9.0.85-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8906cda76255..30ae5a35ea70 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009008503 - versionName "9.0.85-3" + versionCode 1009008504 + versionName "9.0.85-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 22952c20893a..8fc94ef68584 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.85.3 + 9.0.85.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index faebe8bc7337..f7e7fd1062ad 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.85.3 + 9.0.85.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 765d5d4b607f..50002965e8b7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.85 CFBundleVersion - 9.0.85.3 + 9.0.85.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 1aeefc381bd8..d19fa45d10d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.85-3", + "version": "9.0.85-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.85-3", + "version": "9.0.85-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b744f1c679c0..0a12899cccd5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.85-3", + "version": "9.0.85-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From a8d37ad4e5732e21dd92eea3d46623c436a577a6 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Tue, 14 Jan 2025 21:48:26 +0000 Subject: [PATCH 12/98] Update Mobile-Expensify to 9.0.85-4 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 6be7b9e36f93..f263e2957ff5 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 6be7b9e36f93495289c84ad4fef5bea35c76e8d6 +Subproject commit f263e2957ff55e63381232d443751828a558a8e8 From 955ca130f17dfc8b3b58de306a2353b7e79ebf06 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 23 Dec 2024 16:48:37 +0300 Subject: [PATCH 13/98] reset transaction on page refresh for IOU distance page --- src/libs/actions/TransactionEdit.ts | 19 +++++++++++++++---- .../request/step/IOURequestStepDistance.tsx | 6 +++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index 56cb49b4163c..bb70e885e36e 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -8,7 +8,7 @@ let connection: Connection; /** * Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction. */ -function createBackupTransaction(transaction: OnyxEntry) { +function createBackupTransaction(transaction: OnyxEntry, isDraft: boolean) { if (!transaction) { return; } @@ -20,9 +20,20 @@ function createBackupTransaction(transaction: OnyxEntry) { const newTransaction = { ...transaction, }; - - // Use set so that it will always fully overwrite any backup transaction that could have existed before - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, newTransaction); + const conn = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, + callback: (transactionBackup) => { + Onyx.disconnect(conn); + if (transactionBackup) { + // If the transactionBackup exists it means we haven't properly restored original value on unmount + // such as on page refresh, so we will just restore the transaction from the transactionBackup here. + Onyx.set(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transactionBackup); + return; + } + // Use set so that it will always fully overwrite any backup transaction that could have existed before + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, newTransaction); + }, + }); } /** diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 7e4027f10382..227ee5a8d7cc 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -218,9 +218,9 @@ function IOURequestStepDistance({ if (isCreatingNewRequest) { return () => {}; } - + const isDraft = IOUUtils.shouldUseTransactionDraft(action); // On mount, create the backup transaction. - TransactionEdit.createBackupTransaction(transaction); + TransactionEdit.createBackupTransaction(transaction, isDraft); return () => { // If the user cancels out of the modal without without saving changes, then the original transaction @@ -229,7 +229,7 @@ function IOURequestStepDistance({ TransactionEdit.removeBackupTransaction(transaction?.transactionID); return; } - TransactionEdit.restoreOriginalTransactionFromBackup(transaction?.transactionID, IOUUtils.shouldUseTransactionDraft(action)); + TransactionEdit.restoreOriginalTransactionFromBackup(transaction?.transactionID ?? '-1', isDraft); // If the user opens IOURequestStepDistance in offline mode and then goes online, re-open the report to fill in missing fields from the transaction backup if (!transaction?.reportID) { From eff3e36d43b22a139acc7ee355628b3e339edd2a Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 14 Jan 2025 17:02:17 +0300 Subject: [PATCH 14/98] lint fix --- .../request/step/IOURequestStepDistance.tsx | 107 ++++++++++-------- 1 file changed, 59 insertions(+), 48 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index 227ee5a8d7cc..aa8b896e4dcd 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -20,29 +20,40 @@ import useNetwork from '@hooks/useNetwork'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Report from '@libs/actions/Report'; +import { + createDistanceRequest, + getIOURequestPolicyID, + resetSplitShares, + setMoneyRequestAmount, + setMoneyRequestMerchant, + setMoneyRequestParticipantsFromReport, + setMoneyRequestPendingFields, + setSplitShares, + trackExpense, + updateMoneyRequestDistance, +} from '@libs/actions/IOU'; +import {init, stop} from '@libs/actions/MapboxToken'; +import {openReport} from '@libs/actions/Report'; +import {openDraftDistanceExpense, removeWaypoint, updateWaypoints as updateWaypointsUtil} from '@libs/actions/Transaction'; +import {createBackupTransaction, removeBackupTransaction, restoreOriginalTransactionFromBackup} from '@libs/actions/TransactionEdit'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import type {MileageRate} from '@libs/DistanceRequestUtils'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import * as IOUUtils from '@libs/IOUUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; +import {shouldUseTransactionDraft} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {getPersonalPolicy, getPolicy} from '@libs/PolicyUtils'; +import {isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatUtil} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; -import * as MapboxToken from '@userActions/MapboxToken'; -import * as TransactionAction from '@userActions/Transaction'; -import * as TransactionEdit from '@userActions/TransactionEdit'; +import {getDistanceInMeters, getRateID, getRequestType, getValidWaypoints, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import type Transaction from '@src/types/onyx/Transaction'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; @@ -51,7 +62,7 @@ import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type IOURequestStepDistanceProps = WithCurrentUserPersonalDetailsProps & WithWritableReportOrNotFoundProps & { /** The transaction object being modified in Onyx */ - transaction: OnyxEntry; + transaction: OnyxEntry; }; function IOURequestStepDistance({ @@ -91,7 +102,7 @@ function IOURequestStepDistance({ transaction, waypoints, action, - IOUUtils.shouldUseTransactionDraft(action) ? CONST.TRANSACTION.STATE.DRAFT : CONST.TRANSACTION.STATE.CURRENT, + shouldUseTransactionDraft(action) ? CONST.TRANSACTION.STATE.DRAFT : CONST.TRANSACTION.STATE.CURRENT, ); const waypointsList = Object.keys(waypoints); const previousWaypoints = usePrevious(waypoints); @@ -121,8 +132,8 @@ function IOURequestStepDistance({ const transactionWasSaved = useRef(false); const isCreatingNewRequest = !(backTo || isEditing); const [recentWaypoints, {status: recentWaypointsStatus}] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS); - const iouRequestType = TransactionUtils.getRequestType(transaction); - const customUnitRateID = TransactionUtils.getRateID(transaction); + const iouRequestType = getRequestType(transaction); + const customUnitRateID = getRateID(transaction); // Sets `amount` and `split` share data before moving to the next step to avoid briefly showing `0.00` as the split share for participants const setDistanceRequestData = useCallback( @@ -132,26 +143,26 @@ function IOURequestStepDistance({ const selectedReportID = participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID; const policyReport = participants.at(0) ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`] : report; - const IOUpolicyID = IOU.getIOURequestPolicyID(transaction, policyReport); - const IOUpolicy = PolicyUtils.getPolicy(report?.policyID ?? IOUpolicyID); - const policyCurrency = policy?.outputCurrency ?? PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; + const IOUpolicyID = getIOURequestPolicyID(transaction, policyReport); + const IOUpolicy = getPolicy(report?.policyID ?? IOUpolicyID); + const policyCurrency = policy?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; const mileageRates = DistanceRequestUtils.getMileageRates(IOUpolicy); const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(IOUpolicy); - const mileageRate: MileageRate | undefined = TransactionUtils.isCustomUnitRateIDForP2P(transaction) + const mileageRate: MileageRate | undefined = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing (customUnitRateID && mileageRates?.[customUnitRateID]) || defaultMileageRate; const {unit, rate} = mileageRate ?? {}; - const distance = TransactionUtils.getDistanceInMeters(transaction, unit); + const distance = getDistanceInMeters(transaction, unit); const currency = mileageRate?.currency ?? policyCurrency; const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0); - IOU.setMoneyRequestAmount(transactionID, amount, currency); + setMoneyRequestAmount(transactionID, amount, currency); const participantAccountIDs: number[] | undefined = participants?.map((participant) => Number(participant.accountID ?? CONST.DEFAULT_NUMBER_ID)); if (isSplitRequest && amount && currency && !isPolicyExpenseChat) { - IOU.setSplitShares(transaction, amount, currency ?? '', participantAccountIDs ?? []); + setSplitShares(transaction, amount, currency ?? '', participantAccountIDs ?? []); } }, [report, allReports, transaction, transactionID, isSplitRequest, policy?.outputCurrency, reportID, customUnitRateID], @@ -166,8 +177,8 @@ function IOURequestStepDistance({ return ( iouType !== CONST.IOU.TYPE.SPLIT && - !ReportUtils.isArchivedReport(report, reportNameValuePairs) && - !(ReportUtils.isPolicyExpenseChat(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))) + !isArchivedReport(report, reportNameValuePairs) && + !(isPolicyExpenseChatUtil(report) && ((policy?.requiresCategory ?? false) || (policy?.requiresTag ?? false))) ); }, [report, skipConfirmation, policy, reportNameValuePairs, iouType]); let buttonText = !isCreatingNewRequest ? translate('common.save') : translate('common.next'); @@ -188,12 +199,12 @@ function IOURequestStepDistance({ // Only load the recent waypoints if they have been read from Onyx as undefined // If the account doesn't have recent waypoints they will be returned as an empty array - TransactionAction.openDraftDistanceExpense(); + openDraftDistanceExpense(); }, [iouRequestType, recentWaypointsStatus, recentWaypoints, isOffline]); useEffect(() => { - MapboxToken.init(); - return MapboxToken.stop; + init(); + return stop; }, []); useEffect(() => { @@ -218,24 +229,24 @@ function IOURequestStepDistance({ if (isCreatingNewRequest) { return () => {}; } - const isDraft = IOUUtils.shouldUseTransactionDraft(action); + const isDraft = shouldUseTransactionDraft(action); // On mount, create the backup transaction. - TransactionEdit.createBackupTransaction(transaction, isDraft); + createBackupTransaction(transaction, isDraft); return () => { // If the user cancels out of the modal without without saving changes, then the original transaction // needs to be restored from the backup so that all changes are removed. if (transactionWasSaved.current) { - TransactionEdit.removeBackupTransaction(transaction?.transactionID); + removeBackupTransaction(transaction?.transactionID); return; } - TransactionEdit.restoreOriginalTransactionFromBackup(transaction?.transactionID ?? '-1', isDraft); + restoreOriginalTransactionFromBackup(transaction?.transactionID, isDraft); // If the user opens IOURequestStepDistance in offline mode and then goes online, re-open the report to fill in missing fields from the transaction backup if (!transaction?.reportID) { return; } - Report.openReport(transaction?.reportID); + openReport(transaction?.reportID); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); @@ -285,7 +296,7 @@ function IOURequestStepDistance({ const navigateToNextStep = useCallback(() => { if (transaction?.splitShares) { - IOU.resetSplitShares(transaction); + resetSplitShares(transaction); } if (backTo) { Navigation.goBack(backTo); @@ -298,20 +309,20 @@ function IOURequestStepDistance({ // In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. // If the user started this flow using the Create expense option (combined submit/track flow), they should be redirected to the participants page. - if (report?.reportID && !ReportUtils.isArchivedReport(report, reportNameValuePairs) && iouType !== CONST.IOU.TYPE.CREATE) { - const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + if (report?.reportID && !isArchivedReport(report, reportNameValuePairs) && iouType !== CONST.IOU.TYPE.CREATE) { + const selectedParticipants = setMoneyRequestParticipantsFromReport(transactionID, report); const participants = selectedParticipants.map((participant) => { const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID; - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + return participantAccountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant); }); setDistanceRequestData(participants); if (shouldSkipConfirmation) { - IOU.setMoneyRequestPendingFields(transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - IOU.setMoneyRequestMerchant(transactionID, translate('iou.fieldPending'), false); + setMoneyRequestPendingFields(transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + setMoneyRequestMerchant(transactionID, translate('iou.fieldPending'), false); const participant = participants.at(0); if (iouType === CONST.IOU.TYPE.TRACK && participant) { playSound(SOUNDS.DONE); - IOU.trackExpense( + trackExpense( report, 0, transaction?.currency ?? 'USD', @@ -332,7 +343,7 @@ function IOURequestStepDistance({ undefined, undefined, undefined, - TransactionUtils.getValidWaypoints(waypoints, true), + getValidWaypoints(waypoints, true), undefined, undefined, undefined, @@ -343,7 +354,7 @@ function IOURequestStepDistance({ } playSound(SOUNDS.DONE); - IOU.createDistanceRequest({ + createDistanceRequest({ report, participants, currentUserLogin: currentUserPersonalDetails.login, @@ -357,14 +368,14 @@ function IOURequestStepDistance({ currency: transaction?.currency ?? 'USD', merchant: translate('iou.fieldPending'), billable: !!policy?.defaultBillable, - validWaypoints: TransactionUtils.getValidWaypoints(waypoints, true), + validWaypoints: getValidWaypoints(waypoints, true), customUnitRateID: DistanceRequestUtils.getCustomUnitRateID(report.reportID), splitShares: transaction?.splitShares, }, }); return; } - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + setMoneyRequestParticipantsFromReport(transactionID, report); navigateToConfirmationPage(); return; } @@ -394,7 +405,7 @@ function IOURequestStepDistance({ const getError = () => { // Get route error if available else show the invalid number of waypoints error. if (hasRouteError) { - return ErrorUtils.getLatestErrorField(transaction, 'route'); + return getLatestErrorField(transaction, 'route'); } if (duplicateWaypointsError) { return {duplicateWaypointsError: translate('iou.error.duplicateWaypointsErrorMessage')} as Errors; @@ -427,8 +438,8 @@ function IOURequestStepDistance({ setOptimisticWaypoints(newWaypoints); Promise.all([ - TransactionAction.removeWaypoint(transaction, emptyWaypointIndex.toString(), IOUUtils.shouldUseTransactionDraft(action)), - TransactionAction.updateWaypoints(transactionID, newWaypoints, IOUUtils.shouldUseTransactionDraft(action)), + removeWaypoint(transaction, emptyWaypointIndex.toString(), shouldUseTransactionDraft(action)), + updateWaypointsUtil(transactionID, newWaypoints, shouldUseTransactionDraft(action)), ]).then(() => { setOptimisticWaypoints(null); }); @@ -458,7 +469,7 @@ function IOURequestStepDistance({ } if (transaction?.transactionID && report?.reportID) { - IOU.updateMoneyRequestDistance({ + updateMoneyRequestDistance({ transactionID: transaction?.transactionID, transactionThreadReportID: report?.reportID, waypoints, From 881dcd2ebca114db753c181ccc5578ff797998db Mon Sep 17 00:00:00 2001 From: James Dean Date: Tue, 14 Jan 2025 13:55:51 -0800 Subject: [PATCH 15/98] Update CONST.ts Updating per https://github.com/Expensify/App/issues/55244#issuecomment-2591173352 --- src/CONST.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e656194d1d3e..65de8958e2fd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -164,7 +164,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { 'Here’s how to submit an expense:\n' + '\n' + '1. Click the green *+* button.\n' + - '2. Choose *Submit expense*.\n' + + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + '\n' + @@ -5264,7 +5264,7 @@ const CONST = { 'Here’s how to submit an expense:\n' + '\n' + '1. Click the green *+* button.\n' + - '2. Choose *Submit expense*.\n' + + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + '\n' + From 0640a91234703b1c648582d6765bad0b66af221a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 8 Jan 2025 16:17:35 +0100 Subject: [PATCH 16/98] Add TRIP_SUPPORT constant and update link in TripDetailsPage --- src/CONST.ts | 1 + src/pages/Travel/TripDetailsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 65de8958e2fd..36fc2316372e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4551,6 +4551,7 @@ const CONST = { TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), + TRIP_SUPPORT: '/support', SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx index 6236fb249001..2e9df5a6a5e6 100644 --- a/src/pages/Travel/TripDetailsPage.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -128,7 +128,7 @@ function TripDetailsPage({route}: TripDetailsPageProps) { shouldShowRightIcon onPress={() => { setIsTripSupportLoading(true); - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID))?.finally(() => { + Link.openTravelDotLink(activePolicyID, CONST.TRIP_SUPPORT)?.finally(() => { setIsTripSupportLoading(false); }); }} From 122ffba3b5f54b56dcfc18d35f5e5865d97bc537 Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 13 Jan 2025 17:49:35 +0700 Subject: [PATCH 17/98] fix: first item is highlighted in contact list and confirmation page --- src/components/SelectionList/BaseSelectionList.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index f00454099a03..9533a81257ec 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -334,7 +334,10 @@ function BaseSelectionList( isFocused, }); - const selectedItemIndex = useMemo(() => flattenedSections.allOptions.findIndex((option) => option.isSelected), [flattenedSections.allOptions]); + const selectedItemIndex = useMemo( + () => (initiallyFocusedOptionKey ? flattenedSections.allOptions.findIndex((option) => option.isSelected) : -1), + [flattenedSections.allOptions, initiallyFocusedOptionKey], + ); useEffect(() => { if (selectedItemIndex === -1 || selectedItemIndex === focusedIndex) { From 20f00fded2b6626e737a3e3c2f4940e785d7a0db Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 20 Nov 2024 12:23:44 +0100 Subject: [PATCH 18/98] feat: init local library --- ios/tmp.xcconfig | 12 ++ modules/background-task/android/build.gradle | 126 ++++++++++++++++++ .../background-task/android/gradle.properties | 5 + .../android/src/main/AndroidManifest.xml | 3 + .../android/src/main/AndroidManifestNew.xml | 2 + .../ReactNativeBackgroundTaskModule.kt | 24 ++++ .../ReactNativeBackgroundTaskPackage.kt | 35 +++++ .../newarch/ReactNativeBackgroundTaskSpec.kt | 7 + .../oldarch/ReactNativeBackgroundTaskSpec.kt | 11 ++ ...nsify-react-native-background-task.podspec | 41 ++++++ .../ios/ReactNativeBackgroundTask.h | 12 ++ .../ios/ReactNativeBackgroundTask.mm | 27 ++++ modules/background-task/package.json | 19 +++ .../background-task/react-native.config.js | 12 ++ .../src/NativeReactNativeBackgroundTask.ts | 8 ++ modules/background-task/src/index.tsx | 29 ++++ package.json | 3 +- 17 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 ios/tmp.xcconfig create mode 100644 modules/background-task/android/build.gradle create mode 100644 modules/background-task/android/gradle.properties create mode 100644 modules/background-task/android/src/main/AndroidManifest.xml create mode 100644 modules/background-task/android/src/main/AndroidManifestNew.xml create mode 100644 modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt create mode 100644 modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt create mode 100644 modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt create mode 100644 modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt create mode 100644 modules/background-task/expensify-react-native-background-task.podspec create mode 100644 modules/background-task/ios/ReactNativeBackgroundTask.h create mode 100644 modules/background-task/ios/ReactNativeBackgroundTask.mm create mode 100644 modules/background-task/package.json create mode 100644 modules/background-task/react-native.config.js create mode 100644 modules/background-task/src/NativeReactNativeBackgroundTask.ts create mode 100644 modules/background-task/src/index.tsx diff --git a/ios/tmp.xcconfig b/ios/tmp.xcconfig new file mode 100644 index 000000000000..ee98b7b0bd8c --- /dev/null +++ b/ios/tmp.xcconfig @@ -0,0 +1,12 @@ +NEW_EXPENSIFY_URL=https:/$()/new.expensify.com/ +SECURE_EXPENSIFY_URL=https:/$()/secure.expensify.com/ +EXPENSIFY_URL=https:/$()/www.expensify.com/ +EXPENSIFY_PARTNER_NAME=chat-expensify-com +EXPENSIFY_PARTNER_PASSWORD=e21965746fd75f82bb66 +PUSHER_APP_KEY=268df511a204fbb60884 +USE_WEB_PROXY=false +ENVIRONMENT=production +SEND_CRASH_REPORTS=true +FB_API_KEY=AIzaSyBrLKgCuo6Vem6Xi5RPokdumssW8HaWBow +FB_APP_ID=1:1008697809946:web:08de4ecb7656b7235445a3 +FB_PROJECT_ID=expensify-mobile-app diff --git a/modules/background-task/android/build.gradle b/modules/background-task/android/build.gradle new file mode 100644 index 000000000000..335b1fa6bde2 --- /dev/null +++ b/modules/background-task/android/build.gradle @@ -0,0 +1,126 @@ +buildscript { + // Buildscript is evaluated before everything else so we can't use getExtOrDefault + def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ReactNativeBackgroundTask_kotlinVersion"] + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:7.2.1" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +def reactNativeArchitectures() { + def value = rootProject.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +def isNewArchitectureEnabled() { + return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +if (isNewArchitectureEnabled()) { + apply plugin: "com.facebook.react" +} + +def getExtOrDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ReactNativeBackgroundTask_" + name] +} + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeBackgroundTask_" + name]).toInteger() +} + +def supportsNamespace() { + def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') + def major = parsed[0].toInteger() + def minor = parsed[1].toInteger() + + // Namespace support was added in 7.3.0 + return (major == 7 && minor >= 3) || major >= 8 +} + +android { + if (supportsNamespace()) { + namespace "com.expensify.reactnativebackgroundtask" + + sourceSets { + main { + manifest.srcFile "src/main/AndroidManifestNew.xml" + } + } + } + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() + + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + if (isNewArchitectureEnabled()) { + java.srcDirs += [ + "src/newarch", + // Codegen specs + "generated/java", + "generated/jni" + ] + } else { + java.srcDirs += ["src/oldarch"] + } + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + // For < 0.71, this will be from the local maven repo + // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin + //noinspection GradleDynamicVersion + implementation "com.facebook.react:react-native:+" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} + +if (isNewArchitectureEnabled()) { + react { + jsRootDir = file("../src/") + libraryName = "ReactNativeBackgroundTask" + codegenJavaPackageName = "com.expensify.reactnativebackgroundtask" + } +} diff --git a/modules/background-task/android/gradle.properties b/modules/background-task/android/gradle.properties new file mode 100644 index 000000000000..6ed0dc89b17c --- /dev/null +++ b/modules/background-task/android/gradle.properties @@ -0,0 +1,5 @@ +ReactNativeBackgroundTask_kotlinVersion=1.7.0 +ReactNativeBackgroundTask_minSdkVersion=21 +ReactNativeBackgroundTask_targetSdkVersion=31 +ReactNativeBackgroundTask_compileSdkVersion=31 +ReactNativeBackgroundTask_ndkversion=21.4.7075529 diff --git a/modules/background-task/android/src/main/AndroidManifest.xml b/modules/background-task/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..327870652628 --- /dev/null +++ b/modules/background-task/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/modules/background-task/android/src/main/AndroidManifestNew.xml b/modules/background-task/android/src/main/AndroidManifestNew.xml new file mode 100644 index 000000000000..a2f47b6057db --- /dev/null +++ b/modules/background-task/android/src/main/AndroidManifestNew.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt new file mode 100644 index 000000000000..c985b33aa800 --- /dev/null +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt @@ -0,0 +1,24 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise + +class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplicationContext) : + ReactNativeBackgroundTaskSpec(context) { + + override fun getName(): String { + return NAME + } + + // Example method + // See https://reactnative.dev/docs/native-modules-android + @ReactMethod + override fun multiply(a: Double, b: Double, promise: Promise) { + promise.resolve(a * b) + } + + companion object { + const val NAME = "ReactNativeBackgroundTask" + } +} diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt new file mode 100644 index 000000000000..dd0547f7ef35 --- /dev/null +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt @@ -0,0 +1,35 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.TurboReactPackage +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.NativeModule +import com.facebook.react.module.model.ReactModuleInfoProvider +import com.facebook.react.module.model.ReactModuleInfo +import java.util.HashMap + +class ReactNativeBackgroundTaskPackage : TurboReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == ReactNativeBackgroundTaskModule.NAME) { + ReactNativeBackgroundTaskModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val moduleInfos: MutableMap = HashMap() + val isTurboModule: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + moduleInfos[ReactNativeBackgroundTaskModule.NAME] = ReactModuleInfo( + ReactNativeBackgroundTaskModule.NAME, + ReactNativeBackgroundTaskModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + ) + moduleInfos + } + } +} diff --git a/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt new file mode 100644 index 000000000000..2ee39c49c66d --- /dev/null +++ b/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt @@ -0,0 +1,7 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext + +abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : + NativeReactNativeBackgroundTaskSpec(context) { +} diff --git a/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt new file mode 100644 index 000000000000..771e6a13734b --- /dev/null +++ b/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt @@ -0,0 +1,11 @@ +package com.expensify.reactnativebackgroundtask + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.Promise + +abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : + ReactContextBaseJavaModule(context) { + + abstract fun multiply(a: Double, b: Double, promise: Promise) +} diff --git a/modules/background-task/expensify-react-native-background-task.podspec b/modules/background-task/expensify-react-native-background-task.podspec new file mode 100644 index 000000000000..d66739edb3e5 --- /dev/null +++ b/modules/background-task/expensify-react-native-background-task.podspec @@ -0,0 +1,41 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' + +Pod::Spec.new do |s| + s.name = "expensify-react-native-background-task" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => ".git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,cpp}" + + # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. + # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. + if respond_to?(:install_modules_dependencies, true) + install_modules_dependencies(s) + else + s.dependency "React-Core" + + # Don't install the dependencies when we run `pod install` in the old architecture. + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end + end +end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h new file mode 100644 index 000000000000..6e0ac781afd8 --- /dev/null +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -0,0 +1,12 @@ + +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNReactNativeBackgroundTaskSpec.h" + +@interface ReactNativeBackgroundTask : NSObject +#else +#import + +@interface ReactNativeBackgroundTask : NSObject +#endif + +@end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm new file mode 100644 index 000000000000..2f318ca219dd --- /dev/null +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -0,0 +1,27 @@ +#import "ReactNativeBackgroundTask.h" + +@implementation ReactNativeBackgroundTask +RCT_EXPORT_MODULE() + +// Example method +// See // https://reactnative.dev/docs/native-modules-ios +RCT_EXPORT_METHOD(multiply:(double)a + b:(double)b + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + NSNumber *result = @(a * b); + + resolve(result); +} + +// Don't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif + +@end diff --git a/modules/background-task/package.json b/modules/background-task/package.json new file mode 100644 index 000000000000..92d464777bae --- /dev/null +++ b/modules/background-task/package.json @@ -0,0 +1,19 @@ +{ + "name": "@expensify/react-native-background-task", + "version": "0.0.0", + "description": "Execute tasks in background", + "main": "src/index", + "codegenConfig": { + "name": "RNReactNativeBackgroundTaskSpec", + "type": "modules", + "jsSrcsDir": "src" + }, + "author": " <> ()", + "license": "UNLICENSED", + "homepage": "#readme", + "create-react-native-library": { + "type": "module-mixed", + "languages": "kotlin-objc", + "version": "0.44.1" + } +} diff --git a/modules/background-task/react-native.config.js b/modules/background-task/react-native.config.js new file mode 100644 index 000000000000..66211dab797e --- /dev/null +++ b/modules/background-task/react-native.config.js @@ -0,0 +1,12 @@ +/** + * @type {import('@react-native-community/cli-types').UserDependencyConfig} + */ +module.exports = { + dependency: { + platforms: { + android: { + cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', + }, + }, + }, +}; diff --git a/modules/background-task/src/NativeReactNativeBackgroundTask.ts b/modules/background-task/src/NativeReactNativeBackgroundTask.ts new file mode 100644 index 000000000000..c0dad8a46b4e --- /dev/null +++ b/modules/background-task/src/NativeReactNativeBackgroundTask.ts @@ -0,0 +1,8 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + multiply(a: number, b: number): Promise; +} + +export default TurboModuleRegistry.getEnforcing('ReactNativeBackgroundTask'); diff --git a/modules/background-task/src/index.tsx b/modules/background-task/src/index.tsx new file mode 100644 index 000000000000..090c25dfcdc1 --- /dev/null +++ b/modules/background-task/src/index.tsx @@ -0,0 +1,29 @@ +import { NativeModules, Platform } from 'react-native'; + +const LINKING_ERROR = + `The package '@expensify/react-native-background-task' doesn't seem to be linked. Make sure: \n\n` + + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; + +// @ts-expect-error +const isTurboModuleEnabled = global.__turboModuleProxy != null; + +const ReactNativeBackgroundTaskModule = isTurboModuleEnabled + ? require('./NativeReactNativeBackgroundTask').default + : NativeModules.ReactNativeBackgroundTask; + +const ReactNativeBackgroundTask = ReactNativeBackgroundTaskModule + ? ReactNativeBackgroundTaskModule + : new Proxy( + {}, + { + get() { + throw new Error(LINKING_ERROR); + }, + } + ); + +export function multiply(a: number, b: number): Promise { + return ReactNativeBackgroundTask.multiply(a, b); +} diff --git a/package.json b/package.json index 0a12899cccd5..57940d641c06 100644 --- a/package.json +++ b/package.json @@ -188,7 +188,8 @@ "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", - "react-window": "^1.8.9" + "react-window": "^1.8.9", + "@expensify/react-native-background-task": "file:./modules/background-task" }, "devDependencies": { "@actions/core": "1.10.0", From 483f55cabbad694f6b290b9d2e6a758137f67c08 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 20 Nov 2024 12:57:28 +0100 Subject: [PATCH 19/98] feat: add `defineTask` method --- .../ios/ReactNativeBackgroundTask.h | 6 +++- .../ios/ReactNativeBackgroundTask.mm | 34 +++++++++++++++---- .../src/NativeReactNativeBackgroundTask.ts | 6 ++-- modules/background-task/src/index.ts | 24 +++++++++++++ modules/background-task/src/index.tsx | 29 ---------------- src/App.tsx | 8 ++++- src/setup/backgroundTask/index.native.ts | 20 +++++++++++ src/setup/backgroundTask/index.ts | 3 ++ 8 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 modules/background-task/src/index.ts delete mode 100644 modules/background-task/src/index.tsx create mode 100644 src/setup/backgroundTask/index.native.ts create mode 100644 src/setup/backgroundTask/index.ts diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h index 6e0ac781afd8..bd5b425a3b5b 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.h +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -1,4 +1,3 @@ - #ifdef RCT_NEW_ARCH_ENABLED #import "RNReactNativeBackgroundTaskSpec.h" @@ -9,4 +8,9 @@ @interface ReactNativeBackgroundTask : NSObject #endif +- (void)defineTask:(NSString *)taskName + taskExecutor:(RCTResponseSenderBlock)taskExecutor + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; + @end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 2f318ca219dd..6294be9777ad 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -1,18 +1,38 @@ #import "ReactNativeBackgroundTask.h" -@implementation ReactNativeBackgroundTask +@implementation ReactNativeBackgroundTask { + NSMutableDictionary *_taskExecutors; +} + RCT_EXPORT_MODULE() -// Example method -// See // https://reactnative.dev/docs/native-modules-ios -RCT_EXPORT_METHOD(multiply:(double)a - b:(double)b +- (instancetype)init { + if (self = [super init]) { + _taskExecutors = [NSMutableDictionary new]; + } + return self; +} + +RCT_EXPORT_METHOD(defineTask:(NSString *)taskName + taskExecutor:(RCTResponseSenderBlock)taskExecutor resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { - NSNumber *result = @(a * b); + if (taskName == nil) { + NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskName is nil"); + reject(@"ERR_INVALID_TASK_NAME", @"Task name must be provided", nil); + return; + } + + if (taskExecutor == nil) { + NSLog(@"[ReactNativeBackgroundTask] Failed to define task: taskExecutor is nil"); + reject(@"ERR_INVALID_TASK_EXECUTOR", @"Task executor must be provided", nil); + return; + } - resolve(result); + NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); + _taskExecutors[taskName] = taskExecutor; + resolve(nil); } // Don't compile this code when we build for the old architecture. diff --git a/modules/background-task/src/NativeReactNativeBackgroundTask.ts b/modules/background-task/src/NativeReactNativeBackgroundTask.ts index c0dad8a46b4e..ed2a381a4862 100644 --- a/modules/background-task/src/NativeReactNativeBackgroundTask.ts +++ b/modules/background-task/src/NativeReactNativeBackgroundTask.ts @@ -1,8 +1,8 @@ -import type { TurboModule } from 'react-native'; -import { TurboModuleRegistry } from 'react-native'; +import type {TurboModule} from 'react-native'; +import {TurboModuleRegistry} from 'react-native'; export interface Spec extends TurboModule { - multiply(a: number, b: number): Promise; + defineTask(taskName: string, taskExecutor: (data: unknown) => void | Promise): Promise; } export default TurboModuleRegistry.getEnforcing('ReactNativeBackgroundTask'); diff --git a/modules/background-task/src/index.ts b/modules/background-task/src/index.ts new file mode 100644 index 000000000000..bd764e6c779c --- /dev/null +++ b/modules/background-task/src/index.ts @@ -0,0 +1,24 @@ +import NativeReactNativeBackgroundTask from './NativeReactNativeBackgroundTask'; + +type TaskManagerTaskExecutor = (data: T) => void | Promise; + +const TaskManager = { + /** + * Defines a task that can be executed in the background. + * @param taskName - Name of the task. Must be unique and match the name used when registering the task. + * @param taskExecutor - Function that will be executed when the task runs. + */ + defineTask: (taskName: string, taskExecutor: TaskManagerTaskExecutor): Promise => { + if (typeof taskName !== 'string' || taskName.length === 0) { + throw new Error('Task name must be a string'); + } + if (typeof taskExecutor !== 'function') { + throw new Error('Task executor must be a function'); + } + + console.log('native task manager', NativeReactNativeBackgroundTask); + return NativeReactNativeBackgroundTask.defineTask(taskName, taskExecutor); + }, +}; + +export default TaskManager; diff --git a/modules/background-task/src/index.tsx b/modules/background-task/src/index.tsx deleted file mode 100644 index 090c25dfcdc1..000000000000 --- a/modules/background-task/src/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { NativeModules, Platform } from 'react-native'; - -const LINKING_ERROR = - `The package '@expensify/react-native-background-task' doesn't seem to be linked. Make sure: \n\n` + - Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + - '- You rebuilt the app after installing the package\n' + - '- You are not using Expo Go\n'; - -// @ts-expect-error -const isTurboModuleEnabled = global.__turboModuleProxy != null; - -const ReactNativeBackgroundTaskModule = isTurboModuleEnabled - ? require('./NativeReactNativeBackgroundTask').default - : NativeModules.ReactNativeBackgroundTask; - -const ReactNativeBackgroundTask = ReactNativeBackgroundTaskModule - ? ReactNativeBackgroundTaskModule - : new Proxy( - {}, - { - get() { - throw new Error(LINKING_ERROR); - }, - } - ); - -export function multiply(a: number, b: number): Promise { - return ReactNativeBackgroundTask.multiply(a, b); -} diff --git a/src/App.tsx b/src/App.tsx index e11592609350..ad04b45a1dfa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import {PortalProvider} from '@gorhom/portal'; -import React from 'react'; +import React, {useEffect} from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {PickerStateProvider} from 'react-native-picker-select'; @@ -40,6 +40,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; +import registerBackgroundFetch from './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { @@ -64,6 +65,11 @@ function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); + // TODO: move to correct place + useEffect(() => { + registerBackgroundFetch(); + }, []); + return ( diff --git a/src/setup/backgroundTask/index.native.ts b/src/setup/backgroundTask/index.native.ts new file mode 100644 index 000000000000..9ac706ad3e22 --- /dev/null +++ b/src/setup/backgroundTask/index.native.ts @@ -0,0 +1,20 @@ +// import * as BackgroundFetch from 'expo-background-fetch'; +// import {defineTask} from 'expo-task-manager'; +import TaskManager from '@expensify/react-native-background-task'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; + +const BACKGROUND_FETCH_TASK = 'FLUSH-SEQUENTIAL-QUEUE-BACKGROUND-FETCH'; + +TaskManager.defineTask(BACKGROUND_FETCH_TASK, () => { + SequentialQueue.flush(); +}); + +function registerBackgroundFetch() { + // return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { + // minimumInterval: 60 * 15, // 15 minutes + // stopOnTerminate: false, + // startOnBoot: true, + // }); +} + +export default registerBackgroundFetch; diff --git a/src/setup/backgroundTask/index.ts b/src/setup/backgroundTask/index.ts new file mode 100644 index 000000000000..9c1b88dfd8e7 --- /dev/null +++ b/src/setup/backgroundTask/index.ts @@ -0,0 +1,3 @@ +function registerBackgroundFetch() {} + +export default registerBackgroundFetch; From 2e5d8a9d22d52895da8616bc71d91e8a8635b350 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 20 Nov 2024 15:34:13 +0100 Subject: [PATCH 20/98] feat: use BGTaskScheduler api --- ios/NewExpensify/Info.plist | 2 + ios/Podfile | 1 - .../ios/ReactNativeBackgroundTask.h | 5 ++ .../ios/ReactNativeBackgroundTask.mm | 51 +++++++++++++++++++ modules/background-task/src/index.ts | 1 - src/setup/backgroundTask/index.native.ts | 1 + 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 8fc94ef68584..318fc3759167 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -97,6 +97,8 @@ UIBackgroundModes remote-notification + fetch + processing UIFileSharingEnabled diff --git a/ios/Podfile b/ios/Podfile index b74584b839b4..e33b15a0ba3c 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -89,7 +89,6 @@ target 'NewExpensify' do :app_path => "#{Pod::Config.instance.installation_root}/.." ) - target 'NewExpensifyTests' do inherit! :complete # Pods for testing diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h index bd5b425a3b5b..34070299740a 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.h +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -1,9 +1,11 @@ #ifdef RCT_NEW_ARCH_ENABLED #import "RNReactNativeBackgroundTaskSpec.h" +#import @interface ReactNativeBackgroundTask : NSObject #else #import +#import @interface ReactNativeBackgroundTask : NSObject #endif @@ -13,4 +15,7 @@ resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void)handleAppDidFinishLaunching:(NSNotification *)notification; +- (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)); + @end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 6294be9777ad..601cfa268ae4 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -1,4 +1,6 @@ #import "ReactNativeBackgroundTask.h" +#import +#import @implementation ReactNativeBackgroundTask { NSMutableDictionary *_taskExecutors; @@ -9,10 +11,59 @@ @implementation ReactNativeBackgroundTask { - (instancetype)init { if (self = [super init]) { _taskExecutors = [NSMutableDictionary new]; + + // Add observer in the next run loop to ensure proper registration + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleAppDidFinishLaunching:) + name:UIApplicationDidFinishLaunchingNotification + object:nil]; + }); } return self; } +// Add dealloc to properly remove the observer +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)handleAppDidFinishLaunching:(NSNotification *)notification { + NSLog(@"[ReactNativeBackgroundTask] Registering background task handler"); + + if (@available(iOS 13.0, *)) { + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" + usingQueue:dispatch_get_main_queue() + launchHandler:^(__kindof BGTask * _Nonnull task) { + [self handleBackgroundTask:task]; + }]; + } +} + +- (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { + // Create a task request to schedule the next background task + BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; + request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // Schedule for 15 minutes from now + + NSError *error = nil; + if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { + NSLog(@"[ReactNativeBackgroundTask] Failed to schedule next task: %@", error); + } + + // Execute all registered tasks + [_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", taskName); + executor(@[@{ + @"taskName": taskName, + @"type": @"background", + @"identifier": task.identifier + }]); + }]; + + // Mark the task as complete + [task setTaskCompletedWithSuccess:YES]; +} + RCT_EXPORT_METHOD(defineTask:(NSString *)taskName taskExecutor:(RCTResponseSenderBlock)taskExecutor resolve:(RCTPromiseResolveBlock)resolve diff --git a/modules/background-task/src/index.ts b/modules/background-task/src/index.ts index bd764e6c779c..5001c8caa31f 100644 --- a/modules/background-task/src/index.ts +++ b/modules/background-task/src/index.ts @@ -16,7 +16,6 @@ const TaskManager = { throw new Error('Task executor must be a function'); } - console.log('native task manager', NativeReactNativeBackgroundTask); return NativeReactNativeBackgroundTask.defineTask(taskName, taskExecutor); }, }; diff --git a/src/setup/backgroundTask/index.native.ts b/src/setup/backgroundTask/index.native.ts index 9ac706ad3e22..5d452398a693 100644 --- a/src/setup/backgroundTask/index.native.ts +++ b/src/setup/backgroundTask/index.native.ts @@ -10,6 +10,7 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, () => { }); function registerBackgroundFetch() { + console.log('probably not needed'); // return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { // minimumInterval: 60 * 15, // 15 minutes // stopOnTerminate: false, From 18d01f6627e721c46830affce8f9501c8f1e9784 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 26 Nov 2024 17:37:22 +0100 Subject: [PATCH 21/98] fix: remove useless condition --- .../background-task/ios/ReactNativeBackgroundTask.mm | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 601cfa268ae4..f09fb5b452e8 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -31,13 +31,11 @@ - (void)dealloc { - (void)handleAppDidFinishLaunching:(NSNotification *)notification { NSLog(@"[ReactNativeBackgroundTask] Registering background task handler"); - if (@available(iOS 13.0, *)) { - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" - usingQueue:dispatch_get_main_queue() - launchHandler:^(__kindof BGTask * _Nonnull task) { - [self handleBackgroundTask:task]; - }]; - } + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" + usingQueue:dispatch_get_main_queue() + launchHandler:^(__kindof BGTask * _Nonnull task) { + [self handleBackgroundTask:task]; + }]; } - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { From cdb7ec0800ba73e97d938b56e9102eca405c9fe2 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 27 Nov 2024 11:53:06 +0100 Subject: [PATCH 22/98] feat: properly register & add basic handler --- ios/NewExpensify.xcodeproj/project.pbxproj | 4 ++ ios/NewExpensify/AppDelegate.mm | 14 ++++- ios/NewExpensify/Info.plist | 4 ++ .../ios/RNBackgroundTaskManager.h | 22 ++++++++ .../ios/RNBackgroundTaskManager.m | 40 +++++++++++++++ .../ios/ReactNativeBackgroundTask.mm | 51 ++++++++++++++----- 6 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 modules/background-task/ios/RNBackgroundTaskManager.h create mode 100644 modules/background-task/ios/RNBackgroundTaskManager.m diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 1131589c72ec..c8d825589bfb 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 7FD73CA22B23CE9500420AF3 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7FD73C9B2B23CE9500420AF3 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 59A21B2405370FDDD847C813 /* libPods-NewExpensify.a */; }; 9E17CB36A6B22BDD4BE53561 /* libPods-NotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */; }; + AC131FBB2CF634F20010CE80 /* BackgroundTasks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */; }; ACA597C323AA39404655647F /* libPods-NewExpensify-NewExpensifyTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EF33B19FC6A7FE676839430D /* libPods-NewExpensify-NewExpensifyTests.a */; }; BDB853621F354EBB84E619C2 /* ExpensifyNewKansas-MediumItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = D2AFB39EC1D44BF9B91D3227 /* ExpensifyNewKansas-MediumItalic.otf */; }; D27CE6B77196EF3EF450EEAC /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0D3F9E814828D91464DF9D35 /* PrivacyInfo.xcprivacy */; }; @@ -141,6 +142,7 @@ 8B28D84EF339436DBD42A203 /* ExpensifyNeue-BoldItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-BoldItalic.otf"; path = "../assets/fonts/native/ExpensifyNeue-BoldItalic.otf"; sourceTree = ""; }; 8EFE0319D586C1078DB926FD /* Pods-NewExpensify.releaseadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewExpensify.releaseadhoc.xcconfig"; path = "Target Support Files/Pods-NewExpensify/Pods-NewExpensify.releaseadhoc.xcconfig"; sourceTree = ""; }; 9196A72C11B91A52A43D6E8A /* libPods-NotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-NotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = BackgroundTasks.framework; path = System/Library/Frameworks/BackgroundTasks.framework; sourceTree = SDKROOT; }; BBE493797E97F2995E627244 /* Pods-NotificationServiceExtension.debugadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationServiceExtension.debugadhoc.xcconfig"; path = "Target Support Files/Pods-NotificationServiceExtension/Pods-NotificationServiceExtension.debugadhoc.xcconfig"; sourceTree = ""; }; BCD444BEDDB0AF1745B39049 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-NewExpensify-NewExpensifyTests/ExpoModulesProvider.swift"; sourceTree = ""; }; BF6A4C5167244B9FB8E4D4E3 /* ExpensifyNeue-Italic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "ExpensifyNeue-Italic.otf"; path = "../assets/fonts/native/ExpensifyNeue-Italic.otf"; sourceTree = ""; }; @@ -180,6 +182,7 @@ 383643682B6D4AE2005BB9AE /* DeviceCheck.framework in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, E51DC681C7DEE40AEBDDFBFE /* BuildFile in Frameworks */, + AC131FBB2CF634F20010CE80 /* BackgroundTasks.framework in Frameworks */, 8744C5400E24E379441C04A4 /* libPods-NewExpensify.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -239,6 +242,7 @@ 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( + AC131FBA2CF634F20010CE80 /* BackgroundTasks.framework */, 383643672B6D4AE2005BB9AE /* DeviceCheck.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index 5608c44823f4..84f785790c73 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -9,6 +9,8 @@ #import "RCTBootSplash.h" #import "RCTStartupTimer.h" #import +#import +#import @interface AppDelegate () @@ -49,7 +51,17 @@ - (BOOL)application:(UIApplication *)application [UIApplication sharedApplication].applicationIconBadgeNumber = 0; [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isFirstRunComplete"]; } - + + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" + usingQueue:nil + launchHandler:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); + void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; + if (handler) { + handler(task); + } + }]; + return YES; } diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 318fc3759167..747e32e65464 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -100,6 +100,10 @@ fetch processing + BGTaskSchedulerPermittedIdentifiers + + com.szymonrybczak.chat + UIFileSharingEnabled UILaunchStoryboardName diff --git a/modules/background-task/ios/RNBackgroundTaskManager.h b/modules/background-task/ios/RNBackgroundTaskManager.h new file mode 100644 index 000000000000..18c7c3f0c6e9 --- /dev/null +++ b/modules/background-task/ios/RNBackgroundTaskManager.h @@ -0,0 +1,22 @@ +// +// RNBackgroundTaskManager.h +// Pods +// +// Created by Szymon Rybczak on 27/11/2024. +// + + +#import +#import +#import + +@interface RNBackgroundTaskManager : NSObject + +@property (nonatomic, copy) void (^taskHandler)(BGTask * _Nonnull); + ++ (instancetype)shared; +- (void)setHandlerForIdentifier:(NSString *)identifier + completion:(void (^)(BGTask * _Nonnull))handler; +- (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier; + +@end diff --git a/modules/background-task/ios/RNBackgroundTaskManager.m b/modules/background-task/ios/RNBackgroundTaskManager.m new file mode 100644 index 000000000000..585012ed3c93 --- /dev/null +++ b/modules/background-task/ios/RNBackgroundTaskManager.m @@ -0,0 +1,40 @@ +// +// RNBackgroundTaskManager.m +// Pods +// +// Created by Szymon Rybczak on 27/11/2024. +// + +#import + +// RNBackgroundTaskManager.m +@implementation RNBackgroundTaskManager : NSObject { + NSMutableDictionary *_handlers; +} + ++ (instancetype)shared { + static RNBackgroundTaskManager *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[RNBackgroundTaskManager alloc] init]; + }); + return instance; +} + +- (instancetype)init { + if (self = [super init]) { + _handlers = [NSMutableDictionary new]; + } + return self; +} + +- (void)setHandlerForIdentifier:(NSString *)identifier + completion:(void (^)(BGTask * _Nonnull))handler { + _handlers[identifier] = handler; +} + +- (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier { + return _handlers[identifier]; +} + +@end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index f09fb5b452e8..4e4c1eaa9b84 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -1,6 +1,7 @@ #import "ReactNativeBackgroundTask.h" #import #import +#import "RNBackgroundTaskManager.h" @implementation ReactNativeBackgroundTask { NSMutableDictionary *_taskExecutors; @@ -27,19 +28,25 @@ - (instancetype)init { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - -- (void)handleAppDidFinishLaunching:(NSNotification *)notification { - NSLog(@"[ReactNativeBackgroundTask] Registering background task handler"); - - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" - usingQueue:dispatch_get_main_queue() - launchHandler:^(__kindof BGTask * _Nonnull task) { - [self handleBackgroundTask:task]; - }]; -} +// +//- (void)handleAppDidFinishLaunching:(NSNotification *)notification { +// NSLog(@"[ReactNativeBackgroundTask] handleAppDidFinishLaunching"); +// +// if (@available(iOS 13.0, *)) { +// // Ensure we're on the main thread +// if ([NSThread isMainThread]) { +// [self registerBackgroundTask]; +// } else { +// dispatch_async(dispatch_get_main_queue(), ^{ +// [self registerBackgroundTask]; +// }); +// } +// } +//} - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { // Create a task request to schedule the next background task +// BGProcessingTaskRequest TODO: use it BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // Schedule for 15 minutes from now @@ -78,10 +85,30 @@ - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { reject(@"ERR_INVALID_TASK_EXECUTOR", @"Task executor must be provided", nil); return; } - + NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); + + [[RNBackgroundTaskManager shared] setHandlerForIdentifier:@"com.szymonrybczak.chat" completion:^(BGTask * _Nonnull task) { + // Your background task handling code + NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); + [task setTaskCompletedWithSuccess:YES]; + }]; + + + BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; + // Set earliest begin date to some time in the future + request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now + + NSError *error = nil; + if ([[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { + resolve(@YES); + } else { + reject(@"error", error.localizedDescription, error); + } + + _taskExecutors[taskName] = taskExecutor; - resolve(nil); +// resolve(nil); } // Don't compile this code when we build for the old architecture. From 5f6ea86ce6e384915540a6d9d2dff7fa77f0949f Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 27 Nov 2024 12:36:41 +0100 Subject: [PATCH 23/98] feat: execute js function from native --- .../background-task/ios/ReactNativeBackgroundTask.mm | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 4e4c1eaa9b84..7eb31549e8d3 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -89,8 +89,18 @@ - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); [[RNBackgroundTaskManager shared] setHandlerForIdentifier:@"com.szymonrybczak.chat" completion:^(BGTask * _Nonnull task) { - // Your background task handling code NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); + + // Execute all registered tasks + [_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { + NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); + executor(@[@{ + @"taskName": taskName, + @"type": @"background", + @"identifier": task.identifier + }]); + }]; + [task setTaskCompletedWithSuccess:YES]; }]; From 852bf9229c782120ea0a9078a61c42fced1e7938 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 27 Nov 2024 12:37:11 +0100 Subject: [PATCH 24/98] chore: clean up usage on JS side --- src/App.tsx | 7 +------ src/setup/backgroundTask/index.native.ts | 13 ------------- src/setup/backgroundTask/index.ts | 3 --- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ad04b45a1dfa..f95bc7297938 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,7 +40,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; -import registerBackgroundFetch from './setup/backgroundTask'; +import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { @@ -65,11 +65,6 @@ function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); - // TODO: move to correct place - useEffect(() => { - registerBackgroundFetch(); - }, []); - return ( diff --git a/src/setup/backgroundTask/index.native.ts b/src/setup/backgroundTask/index.native.ts index 5d452398a693..f73d5f9a11cd 100644 --- a/src/setup/backgroundTask/index.native.ts +++ b/src/setup/backgroundTask/index.native.ts @@ -1,5 +1,3 @@ -// import * as BackgroundFetch from 'expo-background-fetch'; -// import {defineTask} from 'expo-task-manager'; import TaskManager from '@expensify/react-native-background-task'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; @@ -8,14 +6,3 @@ const BACKGROUND_FETCH_TASK = 'FLUSH-SEQUENTIAL-QUEUE-BACKGROUND-FETCH'; TaskManager.defineTask(BACKGROUND_FETCH_TASK, () => { SequentialQueue.flush(); }); - -function registerBackgroundFetch() { - console.log('probably not needed'); - // return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - // minimumInterval: 60 * 15, // 15 minutes - // stopOnTerminate: false, - // startOnBoot: true, - // }); -} - -export default registerBackgroundFetch; diff --git a/src/setup/backgroundTask/index.ts b/src/setup/backgroundTask/index.ts index 9c1b88dfd8e7..e69de29bb2d1 100644 --- a/src/setup/backgroundTask/index.ts +++ b/src/setup/backgroundTask/index.ts @@ -1,3 +0,0 @@ -function registerBackgroundFetch() {} - -export default registerBackgroundFetch; From ac0336b73946b45349b340903b23d68112afaf51 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 27 Nov 2024 14:17:32 +0100 Subject: [PATCH 25/98] fix: replace `BGProcessingTaskRequest` with `BGProcessingTaskRequest` --- modules/background-task/ios/ReactNativeBackgroundTask.mm | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 7eb31549e8d3..665728db3c13 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -46,8 +46,7 @@ - (void)dealloc { - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { // Create a task request to schedule the next background task -// BGProcessingTaskRequest TODO: use it - BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // Schedule for 15 minutes from now NSError *error = nil; @@ -105,7 +104,7 @@ - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { }]; - BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; // Set earliest begin date to some time in the future request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now From 6163ab657eeaedf9c86c41585da33ff32699f601 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 3 Dec 2024 17:45:44 +0100 Subject: [PATCH 26/98] chore: small code cleaning --- ios/Podfile | 1 + src/App.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/Podfile b/ios/Podfile index e33b15a0ba3c..b74584b839b4 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -89,6 +89,7 @@ target 'NewExpensify' do :app_path => "#{Pod::Config.instance.installation_root}/.." ) + target 'NewExpensifyTests' do inherit! :complete # Pods for testing diff --git a/src/App.tsx b/src/App.tsx index f95bc7297938..420901e4999e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import {PortalProvider} from '@gorhom/portal'; -import React, {useEffect} from 'react'; +import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {PickerStateProvider} from 'react-native-picker-select'; From 71e46b7149e7ebb9ff94fef9f1f9a3bc772d2e96 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 4 Dec 2024 08:54:03 +0100 Subject: [PATCH 27/98] fix: remove android related stuff for now --- modules/background-task/android/build.gradle | 126 ------------------ .../background-task/android/gradle.properties | 5 - .../android/src/main/AndroidManifest.xml | 3 - .../android/src/main/AndroidManifestNew.xml | 2 - .../ReactNativeBackgroundTaskModule.kt | 24 ---- .../ReactNativeBackgroundTaskPackage.kt | 35 ----- .../newarch/ReactNativeBackgroundTaskSpec.kt | 7 - .../oldarch/ReactNativeBackgroundTaskSpec.kt | 11 -- .../background-task/react-native.config.js | 10 +- .../{index.native.ts => index.ios.ts} | 0 10 files changed, 4 insertions(+), 219 deletions(-) delete mode 100644 modules/background-task/android/build.gradle delete mode 100644 modules/background-task/android/gradle.properties delete mode 100644 modules/background-task/android/src/main/AndroidManifest.xml delete mode 100644 modules/background-task/android/src/main/AndroidManifestNew.xml delete mode 100644 modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt delete mode 100644 modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt delete mode 100644 modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt delete mode 100644 modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt rename src/setup/backgroundTask/{index.native.ts => index.ios.ts} (100%) diff --git a/modules/background-task/android/build.gradle b/modules/background-task/android/build.gradle deleted file mode 100644 index 335b1fa6bde2..000000000000 --- a/modules/background-task/android/build.gradle +++ /dev/null @@ -1,126 +0,0 @@ -buildscript { - // Buildscript is evaluated before everything else so we can't use getExtOrDefault - def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["ReactNativeBackgroundTask_kotlinVersion"] - - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "com.android.tools.build:gradle:7.2.1" - // noinspection DifferentKotlinGradleVersion - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -def reactNativeArchitectures() { - def value = rootProject.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - -def isNewArchitectureEnabled() { - return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" -} - -def getExtOrDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["ReactNativeBackgroundTask_" + name] -} - -def getExtOrIntegerDefault(name) { - return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ReactNativeBackgroundTask_" + name]).toInteger() -} - -def supportsNamespace() { - def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') - def major = parsed[0].toInteger() - def minor = parsed[1].toInteger() - - // Namespace support was added in 7.3.0 - return (major == 7 && minor >= 3) || major >= 8 -} - -android { - if (supportsNamespace()) { - namespace "com.expensify.reactnativebackgroundtask" - - sourceSets { - main { - manifest.srcFile "src/main/AndroidManifestNew.xml" - } - } - } - - compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") - - defaultConfig { - minSdkVersion getExtOrIntegerDefault("minSdkVersion") - targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") - buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() - - } - - buildFeatures { - buildConfig true - } - - buildTypes { - release { - minifyEnabled false - } - } - - lintOptions { - disable "GradleCompatible" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - sourceSets { - main { - if (isNewArchitectureEnabled()) { - java.srcDirs += [ - "src/newarch", - // Codegen specs - "generated/java", - "generated/jni" - ] - } else { - java.srcDirs += ["src/oldarch"] - } - } - } -} - -repositories { - mavenCentral() - google() -} - -def kotlin_version = getExtOrDefault("kotlinVersion") - -dependencies { - // For < 0.71, this will be from the local maven repo - // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin - //noinspection GradleDynamicVersion - implementation "com.facebook.react:react-native:+" - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" -} - -if (isNewArchitectureEnabled()) { - react { - jsRootDir = file("../src/") - libraryName = "ReactNativeBackgroundTask" - codegenJavaPackageName = "com.expensify.reactnativebackgroundtask" - } -} diff --git a/modules/background-task/android/gradle.properties b/modules/background-task/android/gradle.properties deleted file mode 100644 index 6ed0dc89b17c..000000000000 --- a/modules/background-task/android/gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -ReactNativeBackgroundTask_kotlinVersion=1.7.0 -ReactNativeBackgroundTask_minSdkVersion=21 -ReactNativeBackgroundTask_targetSdkVersion=31 -ReactNativeBackgroundTask_compileSdkVersion=31 -ReactNativeBackgroundTask_ndkversion=21.4.7075529 diff --git a/modules/background-task/android/src/main/AndroidManifest.xml b/modules/background-task/android/src/main/AndroidManifest.xml deleted file mode 100644 index 327870652628..000000000000 --- a/modules/background-task/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/modules/background-task/android/src/main/AndroidManifestNew.xml b/modules/background-task/android/src/main/AndroidManifestNew.xml deleted file mode 100644 index a2f47b6057db..000000000000 --- a/modules/background-task/android/src/main/AndroidManifestNew.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt deleted file mode 100644 index c985b33aa800..000000000000 --- a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.expensify.reactnativebackgroundtask - -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactMethod -import com.facebook.react.bridge.Promise - -class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplicationContext) : - ReactNativeBackgroundTaskSpec(context) { - - override fun getName(): String { - return NAME - } - - // Example method - // See https://reactnative.dev/docs/native-modules-android - @ReactMethod - override fun multiply(a: Double, b: Double, promise: Promise) { - promise.resolve(a * b) - } - - companion object { - const val NAME = "ReactNativeBackgroundTask" - } -} diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt deleted file mode 100644 index dd0547f7ef35..000000000000 --- a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskPackage.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.expensify.reactnativebackgroundtask - -import com.facebook.react.TurboReactPackage -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.NativeModule -import com.facebook.react.module.model.ReactModuleInfoProvider -import com.facebook.react.module.model.ReactModuleInfo -import java.util.HashMap - -class ReactNativeBackgroundTaskPackage : TurboReactPackage() { - override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { - return if (name == ReactNativeBackgroundTaskModule.NAME) { - ReactNativeBackgroundTaskModule(reactContext) - } else { - null - } - } - - override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { - return ReactModuleInfoProvider { - val moduleInfos: MutableMap = HashMap() - val isTurboModule: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - moduleInfos[ReactNativeBackgroundTaskModule.NAME] = ReactModuleInfo( - ReactNativeBackgroundTaskModule.NAME, - ReactNativeBackgroundTaskModule.NAME, - false, // canOverrideExistingModule - false, // needsEagerInit - true, // hasConstants - false, // isCxxModule - isTurboModule // isTurboModule - ) - moduleInfos - } - } -} diff --git a/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt deleted file mode 100644 index 2ee39c49c66d..000000000000 --- a/modules/background-task/android/src/newarch/ReactNativeBackgroundTaskSpec.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.expensify.reactnativebackgroundtask - -import com.facebook.react.bridge.ReactApplicationContext - -abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : - NativeReactNativeBackgroundTaskSpec(context) { -} diff --git a/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt b/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt deleted file mode 100644 index 771e6a13734b..000000000000 --- a/modules/background-task/android/src/oldarch/ReactNativeBackgroundTaskSpec.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.expensify.reactnativebackgroundtask - -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.bridge.ReactContextBaseJavaModule -import com.facebook.react.bridge.Promise - -abstract class ReactNativeBackgroundTaskSpec internal constructor(context: ReactApplicationContext) : - ReactContextBaseJavaModule(context) { - - abstract fun multiply(a: Double, b: Double, promise: Promise) -} diff --git a/modules/background-task/react-native.config.js b/modules/background-task/react-native.config.js index 66211dab797e..d532440e69b0 100644 --- a/modules/background-task/react-native.config.js +++ b/modules/background-task/react-native.config.js @@ -2,11 +2,9 @@ * @type {import('@react-native-community/cli-types').UserDependencyConfig} */ module.exports = { - dependency: { - platforms: { - android: { - cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', - }, + dependency: { + platforms: { + android: null, + }, }, - }, }; diff --git a/src/setup/backgroundTask/index.native.ts b/src/setup/backgroundTask/index.ios.ts similarity index 100% rename from src/setup/backgroundTask/index.native.ts rename to src/setup/backgroundTask/index.ios.ts From c96a02584b3c0ddd0bdd62236f146d0bad2f87bd Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 10:50:17 +0100 Subject: [PATCH 28/98] fix: replace hardcoded identifier --- ios/NewExpensify/AppDelegate.mm | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index 84f785790c73..c379853dd805 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -52,16 +52,24 @@ - (BOOL)application:(UIApplication *)application [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isFirstRunComplete"]; } - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:@"com.szymonrybczak.chat" - usingQueue:nil - launchHandler:^(BGTask * _Nonnull task) { - NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); - void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; - if (handler) { - handler(task); + NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; + + if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { + NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); + } else { + for (NSString *identifier in backgroundIdentifiers) { + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier + usingQueue:nil + launchHandler:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); + void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; + if (handler) { + handler(task); + } + }]; } - }]; - + } + return YES; } From 2b6cdbb4d5a3a59dd5ecdeec670a492c96c4e698 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 11:19:29 +0100 Subject: [PATCH 29/98] chore: code cleanup --- .../ios/ReactNativeBackgroundTask.mm | 105 +++++++----------- 1 file changed, 38 insertions(+), 67 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 665728db3c13..d52d8a9ba3e5 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -28,45 +28,6 @@ - (instancetype)init { - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } -// -//- (void)handleAppDidFinishLaunching:(NSNotification *)notification { -// NSLog(@"[ReactNativeBackgroundTask] handleAppDidFinishLaunching"); -// -// if (@available(iOS 13.0, *)) { -// // Ensure we're on the main thread -// if ([NSThread isMainThread]) { -// [self registerBackgroundTask]; -// } else { -// dispatch_async(dispatch_get_main_queue(), ^{ -// [self registerBackgroundTask]; -// }); -// } -// } -//} - -- (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { - // Create a task request to schedule the next background task - BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; - request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // Schedule for 15 minutes from now - - NSError *error = nil; - if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { - NSLog(@"[ReactNativeBackgroundTask] Failed to schedule next task: %@", error); - } - - // Execute all registered tasks - [_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { - NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", taskName); - executor(@[@{ - @"taskName": taskName, - @"type": @"background", - @"identifier": task.identifier - }]); - }]; - - // Mark the task as complete - [task setTaskCompletedWithSuccess:YES]; -} RCT_EXPORT_METHOD(defineTask:(NSString *)taskName taskExecutor:(RCTResponseSenderBlock)taskExecutor @@ -85,39 +46,49 @@ - (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)) { return; } - NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); + NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; - [[RNBackgroundTaskManager shared] setHandlerForIdentifier:@"com.szymonrybczak.chat" completion:^(BGTask * _Nonnull task) { - NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); - - // Execute all registered tasks - [_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { - NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); - executor(@[@{ - @"taskName": taskName, - @"type": @"background", - @"identifier": task.identifier - }]); - }]; - - [task setTaskCompletedWithSuccess:YES]; - }]; + if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { + NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); + reject(@"ERR_INVALID_TASK_SCHEDULER_IDENTIFIER", @"No background identifiers found or invalid format", nil); + return; + } + + NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); - BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:@"com.szymonrybczak.chat"]; - // Set earliest begin date to some time in the future - request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now + for (NSString *identifier in backgroundIdentifiers) { + [[RNBackgroundTaskManager shared] setHandlerForIdentifier:identifier completion:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); - NSError *error = nil; - if ([[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { - resolve(@YES); - } else { - reject(@"error", error.localizedDescription, error); - } - - + // Execute all registered tasks + [self->_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { + NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); + executor(@[@{ + @"taskName": taskName, + @"type": @"background", + @"identifier": task.identifier + }]); + }]; + + [task setTaskCompletedWithSuccess:YES]; + }]; + + + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; + + // Set earliest begin date to some time in the future + request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now + + NSError *error = nil; + if ([[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { + resolve(@YES); + } else { + reject(@"error", error.localizedDescription, error); + } + } + _taskExecutors[taskName] = taskExecutor; -// resolve(nil); } // Don't compile this code when we build for the old architecture. From 295136b991e81855245342136b662ce0c34da80e Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 12:09:45 +0100 Subject: [PATCH 30/98] chore: one line impl in `AppDelegate.mm` --- ios/NewExpensify/AppDelegate.mm | 18 +---------------- .../ios/RNBackgroundTaskManager.h | 3 ++- .../ios/RNBackgroundTaskManager.m | 20 +++++++++++++++++++ .../ios/ReactNativeBackgroundTask.h | 3 --- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/ios/NewExpensify/AppDelegate.mm b/ios/NewExpensify/AppDelegate.mm index c379853dd805..5d419f5a623e 100644 --- a/ios/NewExpensify/AppDelegate.mm +++ b/ios/NewExpensify/AppDelegate.mm @@ -52,23 +52,7 @@ - (BOOL)application:(UIApplication *)application [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isFirstRunComplete"]; } - NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; - - if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { - NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); - } else { - for (NSString *identifier in backgroundIdentifiers) { - [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier - usingQueue:nil - launchHandler:^(BGTask * _Nonnull task) { - NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); - void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; - if (handler) { - handler(task); - } - }]; - } - } + [RNBackgroundTaskManager setup]; return YES; } diff --git a/modules/background-task/ios/RNBackgroundTaskManager.h b/modules/background-task/ios/RNBackgroundTaskManager.h index 18c7c3f0c6e9..165150b4219c 100644 --- a/modules/background-task/ios/RNBackgroundTaskManager.h +++ b/modules/background-task/ios/RNBackgroundTaskManager.h @@ -15,7 +15,8 @@ @property (nonatomic, copy) void (^taskHandler)(BGTask * _Nonnull); + (instancetype)shared; -- (void)setHandlerForIdentifier:(NSString *)identifier ++ (void)setup; +- (void)setHandlerForIdentifier:(NSString *)identifier completion:(void (^)(BGTask * _Nonnull))handler; - (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier; diff --git a/modules/background-task/ios/RNBackgroundTaskManager.m b/modules/background-task/ios/RNBackgroundTaskManager.m index 585012ed3c93..f8e665630071 100644 --- a/modules/background-task/ios/RNBackgroundTaskManager.m +++ b/modules/background-task/ios/RNBackgroundTaskManager.m @@ -37,4 +37,24 @@ - (void)setHandlerForIdentifier:(NSString *)identifier return _handlers[identifier]; } ++ (void)setup { + NSArray *backgroundIdentifiers = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"]; + + if (!backgroundIdentifiers || ![backgroundIdentifiers isKindOfClass:[NSArray class]]) { + NSLog(@"[ReactNativeBackgroundTask] No background identifiers found or invalid format"); + } else { + for (NSString *identifier in backgroundIdentifiers) { + [[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:identifier + usingQueue:nil + launchHandler:^(BGTask * _Nonnull task) { + NSLog(@"[ReactNativeBackgroundTask] Executing background task: %@", task.identifier); + void (^handler)(BGTask * _Nonnull) = [[RNBackgroundTaskManager shared] handlerForIdentifier:task.identifier]; + if (handler) { + handler(task); + } + }]; + } + } +} + @end diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h index 34070299740a..8ddedaeb6fad 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.h +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -15,7 +15,4 @@ resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; -- (void)handleAppDidFinishLaunching:(NSNotification *)notification; -- (void)handleBackgroundTask:(BGTask *)task API_AVAILABLE(ios(13.0)); - @end From 7110449c3dfac6124d4270221a09b73afef64483 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 12:11:00 +0100 Subject: [PATCH 31/98] chore: remove header comments --- modules/background-task/ios/RNBackgroundTaskManager.h | 8 -------- modules/background-task/ios/RNBackgroundTaskManager.m | 8 -------- 2 files changed, 16 deletions(-) diff --git a/modules/background-task/ios/RNBackgroundTaskManager.h b/modules/background-task/ios/RNBackgroundTaskManager.h index 165150b4219c..9902149fcfbc 100644 --- a/modules/background-task/ios/RNBackgroundTaskManager.h +++ b/modules/background-task/ios/RNBackgroundTaskManager.h @@ -1,11 +1,3 @@ -// -// RNBackgroundTaskManager.h -// Pods -// -// Created by Szymon Rybczak on 27/11/2024. -// - - #import #import #import diff --git a/modules/background-task/ios/RNBackgroundTaskManager.m b/modules/background-task/ios/RNBackgroundTaskManager.m index f8e665630071..f0bc85dfaa9f 100644 --- a/modules/background-task/ios/RNBackgroundTaskManager.m +++ b/modules/background-task/ios/RNBackgroundTaskManager.m @@ -1,13 +1,5 @@ -// -// RNBackgroundTaskManager.m -// Pods -// -// Created by Szymon Rybczak on 27/11/2024. -// - #import -// RNBackgroundTaskManager.m @implementation RNBackgroundTaskManager : NSObject { NSMutableDictionary *_handlers; } From 0a477c17c94982267e15d2f8ec53ca71de247e4e Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 14:30:35 +0100 Subject: [PATCH 32/98] feat: implement sequential task scheduling --- .../ios/ReactNativeBackgroundTask.mm | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index d52d8a9ba3e5..44196d2f4bc2 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -29,6 +29,22 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +- (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { + BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; + + // Set earliest begin date to some time in the future + request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now + + NSError *error = nil; + BOOL success = [[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]; + + if (!success) { + NSLog(@"[ReactNativeBackgroundTask] Failed to schedule task: %@", error.localizedDescription); + } + + return success; +} + RCT_EXPORT_METHOD(defineTask:(NSString *)taskName taskExecutor:(RCTResponseSenderBlock)taskExecutor resolve:(RCTPromiseResolveBlock)resolve @@ -71,20 +87,18 @@ - (void)dealloc { }]); }]; + [self scheduleNewBackgroundTask:identifier]; + [task setTaskCompletedWithSuccess:YES]; }]; - - BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; - - // Set earliest begin date to some time in the future - request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now - - NSError *error = nil; - if ([[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) { + BOOL success = [self scheduleNewBackgroundTask:identifier]; + + if (success) { + _taskExecutors[taskName] = taskExecutor; resolve(@YES); } else { - reject(@"error", error.localizedDescription, error); + reject(@"error", @"Failed to schedule initial background task", nil); } } From 45f8a4de333203ca5fbbad3195a141abdf8dd2f6 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 9 Dec 2024 14:33:08 +0100 Subject: [PATCH 33/98] chore: remove useless observer --- .../ios/ReactNativeBackgroundTask.mm | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 44196d2f4bc2..4ebfff9529d5 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -12,23 +12,10 @@ @implementation ReactNativeBackgroundTask { - (instancetype)init { if (self = [super init]) { _taskExecutors = [NSMutableDictionary new]; - - // Add observer in the next run loop to ensure proper registration - dispatch_async(dispatch_get_main_queue(), ^{ - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleAppDidFinishLaunching:) - name:UIApplicationDidFinishLaunchingNotification - object:nil]; - }); } return self; } -// Add dealloc to properly remove the observer -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; From c6e365c38e9b3315cf0e137f39318cfcf2dcd233 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 10 Dec 2024 09:49:36 +0100 Subject: [PATCH 34/98] fix: properly order packages in `package.json` --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 57940d641c06..4ac373b95aca 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-live-markdown": "0.1.210", + "@expensify/react-native-background-task": "file:./modules/background-task", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -188,8 +189,7 @@ "react-plaid-link": "3.3.2", "react-web-config": "^1.0.0", "react-webcam": "^7.1.1", - "react-window": "^1.8.9", - "@expensify/react-native-background-task": "file:./modules/background-task" + "react-window": "^1.8.9" }, "devDependencies": { "@actions/core": "1.10.0", From 2692235ba89faa36d0c21f99e1d1b40816169761 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 10 Dec 2024 11:24:48 +0100 Subject: [PATCH 35/98] feat!: use new EventEmitter API --- ios/Podfile.lock | 25 +++++++++++++++++++ .../ios/ReactNativeBackgroundTask.h | 2 +- .../ios/ReactNativeBackgroundTask.mm | 6 +---- .../src/NativeReactNativeBackgroundTask.ts | 2 ++ modules/background-task/src/index.ts | 12 +++++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 58bae61afde8..4ddb40b55336 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -35,6 +35,27 @@ PODS: - EXImageLoader (5.0.0): - ExpoModulesCore - React-Core + - expensify-react-native-background-task (0.0.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - Expo (52.0.14): - ExpoModulesCore - ExpoAsset (11.0.1): @@ -2813,6 +2834,7 @@ DEPENDENCIES: - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXAV (from `../node_modules/expo-av/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`) + - "expensify-react-native-background-task (from `../node_modules/@expensify/react-native-background-task`)" - Expo (from `../node_modules/expo`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoFont (from `../node_modules/expo-font/ios`) @@ -2982,6 +3004,8 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-av/ios" EXImageLoader: :path: "../node_modules/expo-image-loader/ios" + expensify-react-native-background-task: + :path: "../node_modules/@expensify/react-native-background-task" Expo: :path: "../node_modules/expo" ExpoAsset: @@ -3228,6 +3252,7 @@ SPEC CHECKSUMS: DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2 EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d + expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d Expo: 0e7b52be71a24a38d5e919e3040d8f51a8739cd0 ExpoAsset: 8138f2a9ec55ae1ad7c3871448379f7d97692d15 ExpoFont: 7522d869d84ee2ee8093ee997fef5b86f85d856b diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.h b/modules/background-task/ios/ReactNativeBackgroundTask.h index 8ddedaeb6fad..93d1e83a6878 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.h +++ b/modules/background-task/ios/ReactNativeBackgroundTask.h @@ -2,7 +2,7 @@ #import "RNReactNativeBackgroundTaskSpec.h" #import -@interface ReactNativeBackgroundTask : NSObject +@interface ReactNativeBackgroundTask : NativeReactNativeBackgroundTaskSpecBase #else #import #import diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 4ebfff9529d5..c23649fd777a 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -67,11 +67,7 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { // Execute all registered tasks [self->_taskExecutors enumerateKeysAndObjectsUsingBlock:^(NSString *taskName, RCTResponseSenderBlock executor, BOOL *stop) { NSLog(@"[ReactNativeBackgroundTask] Executing task: %@", taskName); - executor(@[@{ - @"taskName": taskName, - @"type": @"background", - @"identifier": task.identifier - }]); + [self emitOnBackgroundTaskExecution:(taskName)]; }]; [self scheduleNewBackgroundTask:identifier]; diff --git a/modules/background-task/src/NativeReactNativeBackgroundTask.ts b/modules/background-task/src/NativeReactNativeBackgroundTask.ts index ed2a381a4862..b71c4bef4bc9 100644 --- a/modules/background-task/src/NativeReactNativeBackgroundTask.ts +++ b/modules/background-task/src/NativeReactNativeBackgroundTask.ts @@ -1,8 +1,10 @@ import type {TurboModule} from 'react-native'; import {TurboModuleRegistry} from 'react-native'; +import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; export interface Spec extends TurboModule { defineTask(taskName: string, taskExecutor: (data: unknown) => void | Promise): Promise; + readonly onBackgroundTaskExecution: EventEmitter; } export default TurboModuleRegistry.getEnforcing('ReactNativeBackgroundTask'); diff --git a/modules/background-task/src/index.ts b/modules/background-task/src/index.ts index 5001c8caa31f..d3afe75a85bc 100644 --- a/modules/background-task/src/index.ts +++ b/modules/background-task/src/index.ts @@ -2,6 +2,16 @@ import NativeReactNativeBackgroundTask from './NativeReactNativeBackgroundTask'; type TaskManagerTaskExecutor = (data: T) => void | Promise; +const taskExecutors = new Map(); + +NativeReactNativeBackgroundTask.onBackgroundTaskExecution((taskName) => { + const executor = taskExecutors.get(taskName); + + if (executor) { + executor(taskName); + } +}); + const TaskManager = { /** * Defines a task that can be executed in the background. @@ -16,6 +26,8 @@ const TaskManager = { throw new Error('Task executor must be a function'); } + taskExecutors.set(taskName, taskExecutor); + return NativeReactNativeBackgroundTask.defineTask(taskName, taskExecutor); }, }; From 3459183774a595e9c1f8e8bcc8e2ecfb9807aa12 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 10 Dec 2024 12:16:12 +0100 Subject: [PATCH 36/98] fix: make 0 warnings --- modules/background-task/ios/RNBackgroundTaskManager.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/background-task/ios/RNBackgroundTaskManager.h b/modules/background-task/ios/RNBackgroundTaskManager.h index 9902149fcfbc..38a8a88e734d 100644 --- a/modules/background-task/ios/RNBackgroundTaskManager.h +++ b/modules/background-task/ios/RNBackgroundTaskManager.h @@ -4,12 +4,12 @@ @interface RNBackgroundTaskManager : NSObject -@property (nonatomic, copy) void (^taskHandler)(BGTask * _Nonnull); +@property (nonatomic, copy) void (^ _Nullable taskHandler)(BGTask * _Nonnull); -+ (instancetype)shared; ++ (instancetype _Nullable )shared; + (void)setup; -- (void)setHandlerForIdentifier:(NSString *)identifier - completion:(void (^)(BGTask * _Nonnull))handler; -- (void (^)(BGTask * _Nonnull))handlerForIdentifier:(NSString *)identifier; +- (void)setHandlerForIdentifier:(NSString *_Nullable)identifier + completion:(void (^_Nullable)(BGTask * _Nonnull))handler; +- (void (^_Nullable)(BGTask * _Nonnull))handlerForIdentifier:(NSString *_Nullable)identifier; @end From 3959933c9eefdb01e317de27fffe42a883fff18f Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 10 Dec 2024 12:21:53 +0100 Subject: [PATCH 37/98] fix: add proper bundle id --- ios/NewExpensify/Info.plist | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 747e32e65464..68b404704f13 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -100,10 +100,10 @@ fetch processing - BGTaskSchedulerPermittedIdentifiers - - com.szymonrybczak.chat - + BGTaskSchedulerPermittedIdentifiers + + com.expensify.chat.dev + UIFileSharingEnabled UILaunchStoryboardName From 2b54acdb5f33c0ffa82a13d6c4ceb356ee69b7e8 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 10 Dec 2024 16:46:54 +0100 Subject: [PATCH 38/98] fix: add release bundle id --- ios/NewExpensify/Info.plist | 1 + 1 file changed, 1 insertion(+) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 68b404704f13..4eb117167f85 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -102,6 +102,7 @@ BGTaskSchedulerPermittedIdentifiers + com.expensify.chat.chat com.expensify.chat.dev UIFileSharingEnabled From 4b0889684b3cf06a42deeecc4f8b17a2716892bc Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 11 Dec 2024 10:15:23 +0100 Subject: [PATCH 39/98] fix: include all bundle identifiers --- ios/NewExpensify/Info.plist | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4eb117167f85..98df368c2e41 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -2,6 +2,12 @@ + BGTaskSchedulerPermittedIdentifiers + + com.chat.expensify.chat + com.expensify.chat.dev + com.expensify.chat.adhoc + CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -100,11 +106,6 @@ fetch processing - BGTaskSchedulerPermittedIdentifiers - - com.expensify.chat.chat - com.expensify.chat.dev - UIFileSharingEnabled UILaunchStoryboardName From e639f40539ada89e884783a7c4548102122eeb12 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 17 Dec 2024 23:30:47 +0100 Subject: [PATCH 40/98] fix: replace `BGProcessingTaskRequest` with `BGAppRefreshTaskRequest` --- modules/background-task/ios/ReactNativeBackgroundTask.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index c23649fd777a..adc227415cff 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -17,7 +17,7 @@ - (instancetype)init { } - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { - BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier]; + BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier]; // Set earliest begin date to some time in the future request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:15 * 60]; // 15 minutes from now From ca79968e57fcc6ed45226979cab46b9f43d75a19 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 17 Dec 2024 23:33:45 +0100 Subject: [PATCH 41/98] fix: call `resolve()` only once when there's multiple identifiers --- .../ios/ReactNativeBackgroundTask.mm | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index adc227415cff..8ac39f3707e5 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -60,6 +60,8 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); + BOOL allSuccess = YES; + for (NSString *identifier in backgroundIdentifiers) { [[RNBackgroundTaskManager shared] setHandlerForIdentifier:identifier completion:^(BGTask * _Nonnull task) { NSLog(@"[ReactNativeBackgroundTask] Executing background task's handler"); @@ -79,11 +81,18 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { if (success) { _taskExecutors[taskName] = taskExecutor; - resolve(@YES); } else { - reject(@"error", @"Failed to schedule initial background task", nil); + allSuccess = NO; + break; } } + + if (allSuccess) { + resolve(@YES); + } else { + reject(@"error", @"Failed to schedule initial background task", nil); + } + _taskExecutors[taskName] = taskExecutor; } From 5af0e0b0b21b096e80f261455c28e1d35c085c9a Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Fri, 20 Dec 2024 17:37:31 +0100 Subject: [PATCH 42/98] fix: use bg task specific id --- ios/NewExpensify/Info.plist | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 98df368c2e41..e2308df8eb6d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -4,9 +4,7 @@ BGTaskSchedulerPermittedIdentifiers - com.chat.expensify.chat - com.expensify.chat.dev - com.expensify.chat.adhoc + com.chat.expensify.backgroundTaskSync CADisableMinimumFrameDurationOnPhone From 70165bc7167b831c0eab9306487f64bb1db5520b Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 30 Dec 2024 12:20:17 +0100 Subject: [PATCH 43/98] fix: remove oudated condition in `Podfile` --- ...nsify-react-native-background-task.podspec | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/modules/background-task/expensify-react-native-background-task.podspec b/modules/background-task/expensify-react-native-background-task.podspec index d66739edb3e5..207cd239c463 100644 --- a/modules/background-task/expensify-react-native-background-task.podspec +++ b/modules/background-task/expensify-react-native-background-task.podspec @@ -16,26 +16,5 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,cpp}" - # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. - # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. - if respond_to?(:install_modules_dependencies, true) - install_modules_dependencies(s) - else - s.dependency "React-Core" - - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end - end + install_modules_dependencies(s) end From 795ad3dbf4d16638fa426f0189a4d2be847a85f1 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 7 Jan 2025 23:33:39 +0100 Subject: [PATCH 44/98] chore: update lock --- package-lock.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index d19fa45d10d9..d058b70a832c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", + "@expensify/react-native-background-task": "file:./modules/background-task", "@expensify/react-native-live-markdown": "0.1.210", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", @@ -285,6 +286,11 @@ "npm": "10.8.2" } }, + "modules/background-task": { + "name": "@expensify/react-native-background-task", + "version": "0.0.0", + "license": "UNLICENSED" + }, "node_modules/@0no-co/graphql.web": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.0.11.tgz", @@ -3630,6 +3636,10 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@expensify/react-native-background-task": { + "resolved": "modules/background-task", + "link": true + }, "node_modules/@expensify/react-native-live-markdown": { "version": "0.1.210", "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.210.tgz", From 9e023fcde3a41a44e99d4cb61a4178ba57cd1fc2 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 7 Jan 2025 23:41:48 +0100 Subject: [PATCH 45/98] fix: eslint warnings --- modules/background-task/src/NativeReactNativeBackgroundTask.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/background-task/src/NativeReactNativeBackgroundTask.ts b/modules/background-task/src/NativeReactNativeBackgroundTask.ts index b71c4bef4bc9..792fe2850552 100644 --- a/modules/background-task/src/NativeReactNativeBackgroundTask.ts +++ b/modules/background-task/src/NativeReactNativeBackgroundTask.ts @@ -2,6 +2,8 @@ import type {TurboModule} from 'react-native'; import {TurboModuleRegistry} from 'react-native'; import type {EventEmitter} from 'react-native/Libraries/Types/CodegenTypes'; +// We need to export the interface inline for proper TypeScript type inference with TurboModules +// eslint-disable-next-line rulesdir/no-inline-named-export, @typescript-eslint/consistent-type-definitions export interface Spec extends TurboModule { defineTask(taskName: string, taskExecutor: (data: unknown) => void | Promise): Promise; readonly onBackgroundTaskExecution: EventEmitter; From 39eed383fa9af471725e08f95d065fb7ff6cc448 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 8 Jan 2025 09:32:02 +0100 Subject: [PATCH 46/98] fix: mock module --- tests/ui/GroupChatNameTests.tsx | 5 +++++ tests/ui/PaginationTest.tsx | 5 +++++ tests/ui/SwitchToExpensifyClassicTest.tsx | 5 +++++ tests/ui/UnreadIndicatorsTest.tsx | 5 +++++ tests/ui/WorkspaceSwitcherTest.tsx | 5 +++++ tests/unit/loginTest.tsx | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index fc383efe4e28..eef08d7cbc40 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -22,6 +22,11 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(50000); +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 8fcd6dbac1d6..59e164180075 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -21,6 +21,11 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); diff --git a/tests/ui/SwitchToExpensifyClassicTest.tsx b/tests/ui/SwitchToExpensifyClassicTest.tsx index c8fe13b7e2e9..35c5ec1e58bb 100644 --- a/tests/ui/SwitchToExpensifyClassicTest.tsx +++ b/tests/ui/SwitchToExpensifyClassicTest.tsx @@ -16,6 +16,11 @@ const USER_A_EMAIL = 'user_a@test.com'; jest.setTimeout(60000); +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + jest.mock('@react-navigation/native'); TestHelper.setupApp(); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 6f4313a9f02c..9509ee81db0d 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -32,6 +32,11 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); diff --git a/tests/ui/WorkspaceSwitcherTest.tsx b/tests/ui/WorkspaceSwitcherTest.tsx index 614ed4e5ab70..3ae442823726 100644 --- a/tests/ui/WorkspaceSwitcherTest.tsx +++ b/tests/ui/WorkspaceSwitcherTest.tsx @@ -18,6 +18,11 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { diff --git a/tests/unit/loginTest.tsx b/tests/unit/loginTest.tsx index d5084299bb08..21b0bdd528f6 100644 --- a/tests/unit/loginTest.tsx +++ b/tests/unit/loginTest.tsx @@ -4,6 +4,11 @@ import 'react-native'; import renderer from 'react-test-renderer'; import App from '@src/App'; +jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention From 3bd5984c7d2e4f6ebeb8ddb929ccea33d00b9b71 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Wed, 8 Jan 2025 09:52:30 +0100 Subject: [PATCH 47/98] feat: add a log when executing background task --- src/setup/backgroundTask/index.ios.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/setup/backgroundTask/index.ios.ts b/src/setup/backgroundTask/index.ios.ts index f73d5f9a11cd..52f8c1d9a66e 100644 --- a/src/setup/backgroundTask/index.ios.ts +++ b/src/setup/backgroundTask/index.ios.ts @@ -1,8 +1,10 @@ import TaskManager from '@expensify/react-native-background-task'; +import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; const BACKGROUND_FETCH_TASK = 'FLUSH-SEQUENTIAL-QUEUE-BACKGROUND-FETCH'; TaskManager.defineTask(BACKGROUND_FETCH_TASK, () => { + Log.info('BackgroundTask', true, `Executing ${BACKGROUND_FETCH_TASK} background task at ${new Date().toISOString()}`); SequentialQueue.flush(); }); From 367f95c06f71090cb6ee5c6c62a2c1d51bbbcbd2 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 13 Jan 2025 20:56:03 +0100 Subject: [PATCH 48/98] fix: move mocks to one file --- jest/setup.ts | 5 +++++ tests/ui/GroupChatNameTests.tsx | 5 ----- tests/ui/PaginationTest.tsx | 5 ----- tests/ui/SwitchToExpensifyClassicTest.tsx | 5 ----- tests/ui/UnreadIndicatorsTest.tsx | 5 ----- tests/ui/WorkspaceSwitcherTest.tsx | 5 ----- tests/unit/loginTest.tsx | 5 ----- 7 files changed, 5 insertions(+), 30 deletions(-) diff --git a/jest/setup.ts b/jest/setup.ts index c575054f7dac..4db3d945ad8f 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -83,6 +83,11 @@ jest.mock('@src/libs/actions/Timing', () => ({ end: jest.fn(), })); +jest.mock('../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ + defineTask: jest.fn(), + onBackgroundTaskExecution: jest.fn(), +})); + // This makes FlatList render synchronously for easier testing. jest.mock( '@react-native/virtualized-lists/Interaction/Batchinator', diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index eef08d7cbc40..fc383efe4e28 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -22,11 +22,6 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(50000); -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 59e164180075..8fcd6dbac1d6 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -21,11 +21,6 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); diff --git a/tests/ui/SwitchToExpensifyClassicTest.tsx b/tests/ui/SwitchToExpensifyClassicTest.tsx index 35c5ec1e58bb..c8fe13b7e2e9 100644 --- a/tests/ui/SwitchToExpensifyClassicTest.tsx +++ b/tests/ui/SwitchToExpensifyClassicTest.tsx @@ -16,11 +16,6 @@ const USER_A_EMAIL = 'user_a@test.com'; jest.setTimeout(60000); -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - jest.mock('@react-navigation/native'); TestHelper.setupApp(); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 9509ee81db0d..6f4313a9f02c 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -32,11 +32,6 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); diff --git a/tests/ui/WorkspaceSwitcherTest.tsx b/tests/ui/WorkspaceSwitcherTest.tsx index 3ae442823726..614ed4e5ab70 100644 --- a/tests/ui/WorkspaceSwitcherTest.tsx +++ b/tests/ui/WorkspaceSwitcherTest.tsx @@ -18,11 +18,6 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct' // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(60000); -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); return { diff --git a/tests/unit/loginTest.tsx b/tests/unit/loginTest.tsx index 21b0bdd528f6..d5084299bb08 100644 --- a/tests/unit/loginTest.tsx +++ b/tests/unit/loginTest.tsx @@ -4,11 +4,6 @@ import 'react-native'; import renderer from 'react-test-renderer'; import App from '@src/App'; -jest.mock('../../modules/background-task/src/NativeReactNativeBackgroundTask', () => ({ - defineTask: jest.fn(), - onBackgroundTaskExecution: jest.fn(), -})); - // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention From be655b9ac0eb8afcec99230110cb3b1b81f9b876 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Mon, 13 Jan 2025 21:52:57 +0100 Subject: [PATCH 49/98] feat: pass error to JS --- .../ios/ReactNativeBackgroundTask.mm | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/modules/background-task/ios/ReactNativeBackgroundTask.mm b/modules/background-task/ios/ReactNativeBackgroundTask.mm index 8ac39f3707e5..f963c928a41e 100644 --- a/modules/background-task/ios/ReactNativeBackgroundTask.mm +++ b/modules/background-task/ios/ReactNativeBackgroundTask.mm @@ -16,7 +16,7 @@ - (instancetype)init { return self; } -- (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { +- (BOOL)scheduleNewBackgroundTask:(NSString *)identifier error:(NSError **)outError { BGAppRefreshTaskRequest *request = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier]; // Set earliest begin date to some time in the future @@ -27,6 +27,9 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { if (!success) { NSLog(@"[ReactNativeBackgroundTask] Failed to schedule task: %@", error.localizedDescription); + if (outError != nil) { + *outError = error; + } } return success; @@ -60,7 +63,8 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { NSLog(@"[ReactNativeBackgroundTask] Defining task: %@", taskName); - BOOL allSuccess = YES; + BOOL allSuccess = YES; + NSError *taskError = nil; for (NSString *identifier in backgroundIdentifiers) { [[RNBackgroundTaskManager shared] setHandlerForIdentifier:identifier completion:^(BGTask * _Nonnull task) { @@ -72,17 +76,20 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { [self emitOnBackgroundTaskExecution:(taskName)]; }]; - [self scheduleNewBackgroundTask:identifier]; + NSError *scheduleError = nil; + [self scheduleNewBackgroundTask:identifier error:&scheduleError]; [task setTaskCompletedWithSuccess:YES]; }]; - BOOL success = [self scheduleNewBackgroundTask:identifier]; + NSError *scheduleError = nil; + BOOL success = [self scheduleNewBackgroundTask:identifier error:&scheduleError]; if (success) { _taskExecutors[taskName] = taskExecutor; } else { allSuccess = NO; + taskError = scheduleError; break; } } @@ -90,10 +97,11 @@ - (BOOL)scheduleNewBackgroundTask:(NSString *)identifier { if (allSuccess) { resolve(@YES); } else { - reject(@"error", @"Failed to schedule initial background task", nil); + reject(@"ERR_SCHEDULE_TASK_FAILED", + taskError.localizedDescription ?: @"Failed to schedule initial background task", + taskError); } - _taskExecutors[taskName] = taskExecutor; } From 93d215d66b566c8d2a226d7908ecd8a81f1f0fd9 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 14 Jan 2025 18:37:54 +0100 Subject: [PATCH 50/98] fix: eslint error --- src/setup/backgroundTask/index.ios.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/setup/backgroundTask/index.ios.ts b/src/setup/backgroundTask/index.ios.ts index 52f8c1d9a66e..e616dbb2dcb8 100644 --- a/src/setup/backgroundTask/index.ios.ts +++ b/src/setup/backgroundTask/index.ios.ts @@ -1,10 +1,10 @@ import TaskManager from '@expensify/react-native-background-task'; import Log from '@libs/Log'; -import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import {flush} from '@libs/Network/SequentialQueue'; const BACKGROUND_FETCH_TASK = 'FLUSH-SEQUENTIAL-QUEUE-BACKGROUND-FETCH'; TaskManager.defineTask(BACKGROUND_FETCH_TASK, () => { Log.info('BackgroundTask', true, `Executing ${BACKGROUND_FETCH_TASK} background task at ${new Date().toISOString()}`); - SequentialQueue.flush(); + flush(); }); From fdad6af7c4823f9f12e6d0e0231294ab92360918 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Mon, 13 Jan 2025 14:30:03 -0800 Subject: [PATCH 51/98] Remove custom report welcome message pressable functionality --- src/components/ReportWelcomeText.tsx | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 08f0d17172a1..fa911120ac56 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -60,7 +60,6 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { )}`, ) .join(', '); - const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); const reportName = ReportUtils.getReportName(report); const shouldShowUsePlusButtonText = (moneyRequestOptions.includes(CONST.IOU.TYPE.PAY) || @@ -105,18 +104,9 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { {isPolicyExpenseChat && (welcomeMessage?.messageHtml ? ( - { - if (!canEditPolicyDescription) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id)); - }} - style={[styles.renderHTML, canEditPolicyDescription ? styles.cursorPointer : styles.cursorText]} - accessibilityLabel={translate('reportDescriptionPage.roomDescription')} - > + - + ) : ( {welcomeMessage.phrase1} From cbc2f468b627786173754242d7eb2d63ef623736 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Mon, 13 Jan 2025 17:14:16 -0800 Subject: [PATCH 52/98] fixed eslint no-restricted-syntax --- src/components/ReportWelcomeText.tsx | 56 ++++++++++++++++++---------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index fa911120ac56..4a062d771ebe 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -6,9 +6,25 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getPolicy} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import { + canEditReportDescription as canEditReportDescriptionUtil, + getDisplayNameForParticipant, + getDisplayNamesWithTooltips, + getParticipantsAccountIDsForDisplay, + getPolicyName, + getReportName, + isArchivedNonExpenseReport, + isChatRoom as isChatRoomUtil, + isConciergeChatReport, + isInvoiceRoom as isInvoiceRoomUtil, + isOptimisticPersonalDetail, + isPolicyExpenseChat as isPolicyExpenseChatUtil, + isSelfDM as isSelfDMUtil, + isSystemChat as isSystemChatUtil, + temporary_getMoneyRequestOptions, +} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; @@ -32,21 +48,21 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const isPolicyExpenseChat = isPolicyExpenseChatUtil(report); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID || undefined}`); - const isArchivedRoom = ReportUtils.isArchivedNonExpenseReport(report, reportNameValuePairs); - const isChatRoom = ReportUtils.isChatRoom(report); - const isSelfDM = ReportUtils.isSelfDM(report); - const isInvoiceRoom = ReportUtils.isInvoiceRoom(report); - const isSystemChat = ReportUtils.isSystemChat(report); + const isArchivedRoom = isArchivedNonExpenseReport(report, reportNameValuePairs); + const isChatRoom = isChatRoomUtil(report); + const isSelfDM = isSelfDMUtil(report); + const isInvoiceRoom = isInvoiceRoomUtil(report); + const isSystemChat = isSystemChatUtil(report); const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM || isInvoiceRoom || isSystemChat); - const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report, undefined, true, true); + const participantAccountIDs = getParticipantsAccountIDsForDisplay(report, undefined, true, true); const isMultipleParticipant = participantAccountIDs.length > 1; - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); + const displayNamesWithTooltips = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy); - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); - const canEditReportDescription = ReportUtils.canEditReportDescription(report, policy); + const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); + const canEditReportDescription = canEditReportDescriptionUtil(report, policy); const {canUseCombinedTrackSubmit} = usePermissions(); const filteredOptions = moneyRequestOptions.filter( (item): item is Exclude => @@ -60,7 +76,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { )}`, ) .join(', '); - const reportName = ReportUtils.getReportName(report); + const reportName = getReportName(report); const shouldShowUsePlusButtonText = (moneyRequestOptions.includes(CONST.IOU.TYPE.PAY) || moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT) || @@ -110,9 +126,9 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { ) : ( {welcomeMessage.phrase1} - {ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)} + {getDisplayNameForParticipant(report?.ownerAccountID)} {welcomeMessage.phrase2} - {ReportUtils.getPolicyName(report)} + {getPolicyName(report)} {welcomeMessage.phrase3} ))} @@ -137,13 +153,13 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { {welcomeMessage.phrase1} {report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL ? ( - {ReportUtils.getDisplayNameForParticipant(report?.invoiceReceiver?.accountID)} + {getDisplayNameForParticipant(report?.invoiceReceiver?.accountID)} ) : ( {getPolicy(report?.invoiceReceiver?.policyID)?.name} )} {` ${translate('common.and')} `} - {ReportUtils.getPolicyName(report)} + {getPolicyName(report)} {welcomeMessage.phrase2} ))} @@ -173,7 +189,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { onPress={navigateToReport} suppressHighlighting > - {ReportUtils.getReportName(report)} + {getReportName(report)} )} {welcomeMessage.phrase2 !== undefined && {welcomeMessage.phrase2}} @@ -196,7 +212,7 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { // eslint-disable-next-line react/no-array-index-key - {ReportUtils.isOptimisticPersonalDetail(accountID) ? ( + {isOptimisticPersonalDetail(accountID) ? ( {displayName} ) : ( )} {shouldShowUsePlusButtonText && {translate('reportActionsView.usePlusButton', {additionalText})}} - {ReportUtils.isConciergeChatReport(report) && {translate('reportActionsView.askConcierge')}} + {isConciergeChatReport(report) && {translate('reportActionsView.askConcierge')}} ); From b472975d05627801e7ef87fd1cb56922edce9a9d Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 14 Jan 2025 00:48:27 -0800 Subject: [PATCH 53/98] renamed imported methods according to naming convention --- src/components/ReportWelcomeText.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 4a062d771ebe..45cb8785e000 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -9,20 +9,20 @@ import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getPolicy} from '@libs/PolicyUtils'; import { - canEditReportDescription as canEditReportDescriptionUtil, + canEditReportDescription as canEditReportDescriptionReportUtils, getDisplayNameForParticipant, getDisplayNamesWithTooltips, getParticipantsAccountIDsForDisplay, getPolicyName, getReportName, isArchivedNonExpenseReport, - isChatRoom as isChatRoomUtil, + isChatRoom as isChatRoomReportUtils, isConciergeChatReport, - isInvoiceRoom as isInvoiceRoomUtil, + isInvoiceRoom as isInvoiceRoomReportUtils, isOptimisticPersonalDetail, - isPolicyExpenseChat as isPolicyExpenseChatUtil, - isSelfDM as isSelfDMUtil, - isSystemChat as isSystemChatUtil, + isPolicyExpenseChat as isPolicyExpenseChatReportUtils, + isSelfDM as isSelfDMReportUtils, + isSystemChat as isSystemChatReportUtils, temporary_getMoneyRequestOptions, } from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -48,21 +48,21 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const isPolicyExpenseChat = isPolicyExpenseChatUtil(report); + const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(report); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID || undefined}`); const isArchivedRoom = isArchivedNonExpenseReport(report, reportNameValuePairs); - const isChatRoom = isChatRoomUtil(report); - const isSelfDM = isSelfDMUtil(report); - const isInvoiceRoom = isInvoiceRoomUtil(report); - const isSystemChat = isSystemChatUtil(report); + const isChatRoom = isChatRoomReportUtils(report); + const isSelfDM = isSelfDMReportUtils(report); + const isInvoiceRoom = isInvoiceRoomReportUtils(report); + const isSystemChat = isSystemChatReportUtils(report); const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM || isInvoiceRoom || isSystemChat); const participantAccountIDs = getParticipantsAccountIDsForDisplay(report, undefined, true, true); const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy); const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); - const canEditReportDescription = canEditReportDescriptionUtil(report, policy); + const canEditReportDescription = canEditReportDescriptionReportUtils(report, policy); const {canUseCombinedTrackSubmit} = usePermissions(); const filteredOptions = moneyRequestOptions.filter( (item): item is Exclude => From d6b1e7b0e46ad0d23d73e3d1f38072f33a15f80d Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 14 Jan 2025 11:47:33 -0800 Subject: [PATCH 54/98] applied changes for #rooms and invoice rooms --- src/components/ReportWelcomeText.tsx | 32 +------ src/pages/home/HeaderView.tsx | 137 ++++++++++++++------------- 2 files changed, 74 insertions(+), 95 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 45cb8785e000..378933e1a89e 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -9,7 +9,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getPolicy} from '@libs/PolicyUtils'; import { - canEditReportDescription as canEditReportDescriptionReportUtils, getDisplayNameForParticipant, getDisplayNamesWithTooltips, getParticipantsAccountIDsForDisplay, @@ -31,7 +30,6 @@ import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; -import {PressableWithoutFeedback} from './Pressable'; import RenderHTML from './RenderHTML'; import Text from './Text'; import UserDetailsTooltip from './UserDetailsTooltip'; @@ -62,7 +60,6 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { const displayNamesWithTooltips = getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy); const moneyRequestOptions = temporary_getMoneyRequestOptions(report, policy, participantAccountIDs); - const canEditReportDescription = canEditReportDescriptionReportUtils(report, policy); const {canUseCombinedTrackSubmit} = usePermissions(); const filteredOptions = moneyRequestOptions.filter( (item): item is Exclude => @@ -135,19 +132,9 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { {isInvoiceRoom && !isArchivedRoom && (welcomeMessage?.messageHtml ? ( - { - if (!canEditReportDescription) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID, activeRoute)); - }} - style={[styles.renderHTML, canEditReportDescription ? styles.cursorPointer : styles.cursorText]} - accessibilityLabel={translate('reportDescriptionPage.roomDescription')} - > + - + ) : ( {welcomeMessage.phrase1} @@ -166,20 +153,9 @@ function ReportWelcomeText({report, policy}: ReportWelcomeTextProps) { {isChatRoom && (!isInvoiceRoom || isArchivedRoom) && (welcomeMessage?.messageHtml ? ( - { - const activeRoute = Navigation.getActiveRoute(); - if (canEditReportDescription) { - Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID, activeRoute)); - return; - } - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID, activeRoute)); - }} - style={styles.renderHTML} - accessibilityLabel={translate('reportDescriptionPage.roomDescription')} - > + - + ) : ( {welcomeMessage.phrase1} diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 47b3e09e6a50..ebfab1acaace 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -8,8 +8,7 @@ import CaretWrapper from '@components/CaretWrapper'; import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; +import {BackArrow, DotIndicator, FallbackAvatar} from '@components/Icon/Expensicons'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; @@ -26,18 +25,43 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; -import * as ReportUtils from '@libs/ReportUtils'; +import { + canJoinChat, + getChatRoomSubtitle, + getDisplayNamesWithTooltips, + getIcons, + getParentNavigationSubtitle, + getParticipantsAccountIDsForDisplay, + getPolicyDescriptionText, + getPolicyName, + getReportDescription, + getReportName, + hasReportNameError, + isChatRoom as isChatRoomReportUtils, + isChatThread as isChatThreadReportUtils, + isChatUsedForOnboarding as isChatUsedForOnboardingReportUtils, + isCurrentUserSubmitter, + isDeprecatedGroupDM, + isExpenseRequest, + isGroupChat as isGroupChatReportUtils, + isOpenTaskReport, + isPolicyExpenseChat as isPolicyExpenseChatReportUtils, + isSelfDM as isSelfDMReportUtils, + isTaskReport as isTaskReportReportUtils, + navigateToDetailsPage, + shouldDisableDetailPage as shouldDisableDetailPageReportUtils, + shouldReportShowSubscript, +} from '@libs/ReportUtils'; import FreeTrial from '@pages/settings/Subscription/FreeTrial'; -import * as Report from '@userActions/Report'; -import * as Session from '@userActions/Session'; -import * as Task from '@userActions/Task'; +import {joinRoom} from '@userActions/Report'; +import {checkIfActionIsAllowed} from '@userActions/Session'; +import {deleteTask} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; +import type {Report, ReportAction} from '@src/types/onyx'; import type {Icon as IconType} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -46,10 +70,10 @@ type HeaderViewProps = { onNavigationMenuButtonClicked: () => void; /** The report currently being looked at */ - report: OnyxEntry; + report: OnyxEntry; /** The report action the transaction is tied to from the parent report */ - parentReportAction: OnyxEntry | null; + parentReportAction: OnyxEntry | null; /** The reportID of the current report */ reportID: string; @@ -65,7 +89,7 @@ const fallbackIcon: IconType = { id: -1, }; -function HeaderView({report, parentReportAction, reportID, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) { +function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) { // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); @@ -81,28 +105,28 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const isSelfDM = ReportUtils.isSelfDM(report); - const isGroupChat = ReportUtils.isGroupChat(report) || ReportUtils.isDeprecatedGroupDM(report); + const isSelfDM = isSelfDMReportUtils(report); + const isGroupChat = isGroupChatReportUtils(report) || isDeprecatedGroupDM(report); - const participants = ReportUtils.getParticipantsAccountIDsForDisplay(report, false, true).slice(0, 5); + const participants = getParticipantsAccountIDsForDisplay(report, false, true).slice(0, 5); const isMultipleParticipant = participants.length > 1; - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); + const participantPersonalDetails = getPersonalDetailsForAccountIDs(participants, personalDetails); + const displayNamesWithTooltips = getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant, undefined, isSelfDM); - const isChatThread = ReportUtils.isChatThread(report); - const isChatRoom = ReportUtils.isChatRoom(report); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); - const isTaskReport = ReportUtils.isTaskReport(report); + const isChatThread = isChatThreadReportUtils(report); + const isChatRoom = isChatRoomReportUtils(report); + const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(report); + const isTaskReport = isTaskReportReportUtils(report); const reportHeaderData = !isTaskReport && !isChatThread && report?.parentReportID ? parentReport : report; // Use sorted display names for the title for group chats on native small screen widths - const title = ReportUtils.getReportName(reportHeaderData, policy, parentReportAction, personalDetails, invoiceReceiverPolicy); - const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); - const reportDescription = Parser.htmlToText(ReportUtils.getReportDescription(report)); - const policyName = ReportUtils.getPolicyName(report, true); - const policyDescription = ReportUtils.getPolicyDescriptionText(policy); - const isPersonalExpenseChat = isPolicyExpenseChat && ReportUtils.isCurrentUserSubmitter(report?.reportID); + const title = getReportName(reportHeaderData, policy, parentReportAction, personalDetails, invoiceReceiverPolicy); + const subtitle = getChatRoomSubtitle(reportHeaderData); + const parentNavigationSubtitleData = getParentNavigationSubtitle(reportHeaderData); + const reportDescription = Parser.htmlToText(getReportDescription(report)); + const policyName = getPolicyName(report, true); + const policyDescription = getPolicyDescriptionText(policy); + const isPersonalExpenseChat = isPolicyExpenseChat && isCurrentUserSubmitter(report?.reportID); const shouldShowSubtitle = () => { if (!subtitle) { return false; @@ -116,9 +140,9 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto return true; }; - const join = Session.checkIfActionIsAllowed(() => Report.joinRoom(report)); + const join = checkIfActionIsAllowed(() => joinRoom(report)); - const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); + const canJoin = canJoinChat(report, parentReportAction, policy); const joinButton = (