From 8b33174b58337575a6a0da8f1e7ce164216d4d3d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 21 Oct 2026 18:36:40 +0530 Subject: [PATCH 1/6] Open a support ticket straight from the Talk to a human link --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/hooks/useOpenSupportTicket.ts | 32 ++++++++++++++ .../Navigators/SettingsSplitNavigator.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 ++ src/libs/Navigation/types.ts | 1 + src/pages/settings/HelpPage/HelpPage.tsx | 18 ++------ .../settings/HelpPage/TalkToHumanPage.tsx | 42 +++++++++++++++++++ 8 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 src/hooks/useOpenSupportTicket.ts create mode 100644 src/pages/settings/HelpPage/TalkToHumanPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7a045d50074d..138f7085b031 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2786,6 +2786,7 @@ const ROUTES = { SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', SETTINGS_TROUBLESHOOT_SERVER: 'settings/troubleshoot/server', SETTINGS_HELP: 'settings/help', + SETTINGS_HELP_TALK_TO_HUMAN: 'settings/help/talk-to-human', SETTINGS_SAVE_THE_WORLD: 'settings/teachersunite', SETTINGS_SAVE_THE_WORLD_ADD_PAYMENT_CARD: 'settings/teachersunite/add-payment-card', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1bd0adb6f895..6e96e5b31596 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -98,6 +98,7 @@ const SCREENS = { TROUBLESHOOT: 'Settings_Troubleshoot', TROUBLESHOOT_SERVER: 'Settings_Troubleshoot_Server', HELP: 'Settings_Help', + HELP_TALK_TO_HUMAN: 'Settings_Help_TalkToHuman', EMAIL_ISSUE: 'Settings_Email_Issue', DYNAMIC_VERIFY_ACCOUNT: 'Dynamic_Verify_Account', DYNAMIC_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT: 'Dynamic_Add_Bank_Account_Verify_Account', diff --git a/src/hooks/useOpenSupportTicket.ts b/src/hooks/useOpenSupportTicket.ts new file mode 100644 index 000000000000..fe9294d3dfbf --- /dev/null +++ b/src/hooks/useOpenSupportTicket.ts @@ -0,0 +1,32 @@ +import Growl from '@libs/Growl'; +import Navigation from '@libs/Navigation/Navigation'; + +import {isNoSupportRepAvailableResponse, openSupportTicket} from '@userActions/Report'; + +import useLocalize from './useLocalize'; +import useOpenConciergeAnywhere from './useOpenConciergeAnywhere'; + +/** + * Returns a callback that opens a support ticket, falling back to Concierge when no support rep is free. + * Shared by the Help page row and the Talk to a human route so both behave the same. + */ +function useOpenSupportTicket() { + const {translate} = useLocalize(); + const {openConciergeAnywhere} = useOpenConciergeAnywhere(); + + const openSupportTicketOrConcierge = () => + openSupportTicket() + .then((response) => { + if (!isNoSupportRepAvailableResponse(response)) { + return; + } + + Growl.error(translate('supportTicket.noSupportRepAvailable')); + Navigation.goBack(undefined, {afterTransition: () => openConciergeAnywhere({forceConcierge: true})}); + }) + .catch(() => undefined); + + return {openSupportTicketOrConcierge}; +} + +export default useOpenSupportTicket; diff --git a/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx index 38fde12730d9..9e620a1c5374 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/SettingsSplitNavigator.tsx @@ -25,6 +25,7 @@ const CENTRAL_PANE_SETTINGS_SCREENS = { [SCREENS.SETTINGS.AGENTS.ROOT]: withAgentAccessDenied(() => require('../../../../pages/settings/Agents/AgentsPage').default), [SCREENS.SETTINGS.RULES.ROOT]: () => require('../../../../pages/settings/Rules/ExpenseRulesPage').default, [SCREENS.SETTINGS.HELP]: () => require('../../../../pages/settings/HelpPage/HelpPage').default, + [SCREENS.SETTINGS.HELP_TALK_TO_HUMAN]: () => require('../../../../pages/settings/HelpPage/TalkToHumanPage').default, [SCREENS.SETTINGS.ABOUT]: () => require('../../../../pages/settings/AboutPage/AboutPage').default, [SCREENS.SETTINGS.TROUBLESHOOT]: () => require('../../../../pages/settings/Troubleshoot/TroubleshootPage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1bdfa5e07ddb..878a31682b36 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -2469,6 +2469,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_HELP, exact: true, }, + [SCREENS.SETTINGS.HELP_TALK_TO_HUMAN]: { + path: ROUTES.SETTINGS_HELP_TALK_TO_HUMAN, + exact: true, + }, [SCREENS.SETTINGS.TROUBLESHOOT]: { path: ROUTES.SETTINGS_TROUBLESHOOT, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fecf0231d182..10771b42d82f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1042,6 +1042,7 @@ type SettingsSplitNavigatorParamList = { [SCREENS.SETTINGS.RULES.ROOT]: undefined; [SCREENS.SETTINGS.ABOUT]: undefined; [SCREENS.SETTINGS.HELP]: undefined; + [SCREENS.SETTINGS.HELP_TALK_TO_HUMAN]: undefined; [SCREENS.SETTINGS.TROUBLESHOOT]: undefined; [SCREENS.SETTINGS.SAVE_THE_WORLD]: undefined; [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]?: { diff --git a/src/pages/settings/HelpPage/HelpPage.tsx b/src/pages/settings/HelpPage/HelpPage.tsx index 97a6d58d298b..7958aeff77c4 100644 --- a/src/pages/settings/HelpPage/HelpPage.tsx +++ b/src/pages/settings/HelpPage/HelpPage.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useOpenConciergeAnywhere from '@hooks/useOpenConciergeAnywhere'; +import useOpenSupportTicket from '@hooks/useOpenSupportTicket'; import usePersonalDetailByLogin from '@hooks/usePersonalDetailByLogin'; import {useAllPersonalDetails} from '@hooks/usePersonalDetails'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -21,8 +22,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {openHelpPage} from '@libs/actions/Help'; import {openExternalLink} from '@libs/actions/Link'; -import {isNoSupportRepAvailableResponse, navigateToAndOpenReportWithAccountIDs, openSupportTicket} from '@libs/actions/Report'; -import Growl from '@libs/Growl'; +import {navigateToAndOpenReportWithAccountIDs} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import colors from '@styles/theme/colors'; @@ -64,21 +64,9 @@ function HelpPage() { const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {openConciergeAnywhere} = useOpenConciergeAnywhere(); + const {openSupportTicketOrConcierge} = useOpenSupportTicket(); const {isOffline} = useNetwork(); - const openSupportTicketOrConcierge = () => { - openSupportTicket() - .then((response) => { - if (!isNoSupportRepAvailableResponse(response)) { - return; - } - - Growl.error(translate('supportTicket.noSupportRepAvailable')); - Navigation.goBack(undefined, {afterTransition: () => openConciergeAnywhere({forceConcierge: true})}); - }) - .catch(() => undefined); - }; - // Remove the row's accessibility grouping so native (iOS/Android) screen readers can announce the nested // Book a call button as its own element; on web this prop is a no-op and the button is reached via keyboard Tab instead const shouldBeAccessibleWithBookCallButton = (calendarLink: string | undefined) => !calendarLink; diff --git a/src/pages/settings/HelpPage/TalkToHumanPage.tsx b/src/pages/settings/HelpPage/TalkToHumanPage.tsx new file mode 100644 index 000000000000..5f2b98be0bbb --- /dev/null +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -0,0 +1,42 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; + +import useNetwork from '@hooks/useNetwork'; +import useOpenSupportTicket from '@hooks/useOpenSupportTicket'; + +import Navigation from '@libs/Navigation/Navigation'; + +import ROUTES from '@src/ROUTES'; + +import React, {useEffect, useRef} from 'react'; + +/** + * Opens a support ticket straight away, so the Talk to a human link in a Concierge message reaches a + * person without the customer having to find the row on the Help page. + */ +function TalkToHumanPage() { + const {openSupportTicketOrConcierge} = useOpenSupportTicket(); + const {isOffline} = useNetwork(); + const hasRequestedTicket = useRef(false); + + useEffect(() => { + // Creating a ticket needs the network, so send the customer to the Help page to try again once they are back online. + if (isOffline) { + Navigation.goBack(ROUTES.SETTINGS_HELP); + return; + } + + // A remount would otherwise open a second ticket that a support rep has to close. + if (hasRequestedTicket.current) { + return; + } + hasRequestedTicket.current = true; + openSupportTicketOrConcierge(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOffline]); + + return ; +} + +TalkToHumanPage.displayName = 'TalkToHumanPage'; + +export default TalkToHumanPage; From 27bbef5ff4b41d704fb56db8f21f6ba285c23a85 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 21 Oct 2026 20:33:02 +0530 Subject: [PATCH 2/6] Leave the action route before opening the ticket and justify the hook suppression --- src/pages/settings/HelpPage/TalkToHumanPage.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/HelpPage/TalkToHumanPage.tsx b/src/pages/settings/HelpPage/TalkToHumanPage.tsx index 5f2b98be0bbb..5f040dda635c 100644 --- a/src/pages/settings/HelpPage/TalkToHumanPage.tsx +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -30,11 +30,17 @@ function TalkToHumanPage() { return; } hasRequestedTicket.current = true; - openSupportTicketOrConcierge(); + + // Leave this route before creating the ticket. The ticket stores the route it was opened from as its back target, + // so opening it from here would make Back return to this loader instead of the chat the link was pressed in. + Navigation.goBack(undefined, {afterTransition: openSupportTicketOrConcierge}); + + // openSupportTicketOrConcierge is a new function on every render and hasRequestedTicket already stops a second + // request, so this effect only needs to react to the connection changing. // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOffline]); - return ; + return ; } TalkToHumanPage.displayName = 'TalkToHumanPage'; From 8bb3d5b37bedf67ebe2a11066839cf232e1fe33a Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Wed, 21 Oct 2026 20:34:04 +0530 Subject: [PATCH 3/6] Fall back to the Help page when a direct link has nothing to go back to --- src/pages/settings/HelpPage/TalkToHumanPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/HelpPage/TalkToHumanPage.tsx b/src/pages/settings/HelpPage/TalkToHumanPage.tsx index 5f040dda635c..fa470773346b 100644 --- a/src/pages/settings/HelpPage/TalkToHumanPage.tsx +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -4,6 +4,7 @@ import useNetwork from '@hooks/useNetwork'; import useOpenSupportTicket from '@hooks/useOpenSupportTicket'; import Navigation from '@libs/Navigation/Navigation'; +import navigationRef from '@libs/Navigation/navigationRef'; import ROUTES from '@src/ROUTES'; @@ -33,7 +34,10 @@ function TalkToHumanPage() { // Leave this route before creating the ticket. The ticket stores the route it was opened from as its back target, // so opening it from here would make Back return to this loader instead of the chat the link was pressed in. - Navigation.goBack(undefined, {afterTransition: openSupportTicketOrConcierge}); + // A direct link has nothing behind it to return to and goBack would then skip afterTransition, so it falls back + // to the Help page and opens the ticket from there. + const fallbackRoute = navigationRef.current?.canGoBack() ? undefined : ROUTES.SETTINGS_HELP; + Navigation.goBack(fallbackRoute, {afterTransition: openSupportTicketOrConcierge}); // openSupportTicketOrConcierge is a new function on every render and hasRequestedTicket already stops a second // request, so this effect only needs to react to the connection changing. From 37a9c923c90dff74a337548b68908c34f774fd8b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 22 Oct 2026 03:31:52 +0530 Subject: [PATCH 4/6] Fix the Talk to a human afterTransition type --- src/pages/settings/HelpPage/TalkToHumanPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/HelpPage/TalkToHumanPage.tsx b/src/pages/settings/HelpPage/TalkToHumanPage.tsx index fa470773346b..bfa43e9ae757 100644 --- a/src/pages/settings/HelpPage/TalkToHumanPage.tsx +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -37,7 +37,11 @@ function TalkToHumanPage() { // A direct link has nothing behind it to return to and goBack would then skip afterTransition, so it falls back // to the Help page and opens the ticket from there. const fallbackRoute = navigationRef.current?.canGoBack() ? undefined : ROUTES.SETTINGS_HELP; - Navigation.goBack(fallbackRoute, {afterTransition: openSupportTicketOrConcierge}); + Navigation.goBack(fallbackRoute, { + afterTransition: () => { + openSupportTicketOrConcierge(); + }, + }); // openSupportTicketOrConcierge is a new function on every render and hasRequestedTicket already stops a second // request, so this effect only needs to react to the connection changing. From 723f39a201f7b87298696421a48e3a92814d45eb Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 22 Oct 2026 03:44:34 +0530 Subject: [PATCH 5/6] Set shouldUseGoBackButton explicitly on the Talk to a human loader --- src/pages/settings/HelpPage/TalkToHumanPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/HelpPage/TalkToHumanPage.tsx b/src/pages/settings/HelpPage/TalkToHumanPage.tsx index bfa43e9ae757..c2c3be5c906c 100644 --- a/src/pages/settings/HelpPage/TalkToHumanPage.tsx +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -48,7 +48,7 @@ function TalkToHumanPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOffline]); - return ; + return ; } TalkToHumanPage.displayName = 'TalkToHumanPage'; From c49ecdd81ba8993feecd8ba5952400bb01e6be46 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 23 Oct 2026 23:17:26 +0530 Subject: [PATCH 6/6] Restore upstream formatting of files reformatted by the merge commit hook --- .../AppNavigator/AuthScreensInitHandler.tsx | 16 ++------------ src/libs/TransactionUtils/index.ts | 14 +++++++------ .../ComposerWithSuggestions.tsx | 21 +++++++++++-------- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index 067e69f89324..b46f91efc8bb 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -160,13 +160,7 @@ function AuthScreensInitHandler() { return; } // This means sign in in RHP was successful, so we can subscribe to user events - initializePusher( - session?.accountID, - session?.email, - () => topmostOneTransactionThreadReportIDRef.current, - formatPhoneNumber, - () => reportAttributesRef.current, - ); + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, formatPhoneNumber, () => reportAttributesRef.current); // eslint-disable-next-line react-hooks/exhaustive-deps -- This handler should only be re-registered when the session changes. }, [session?.accountID, session?.email]); @@ -195,13 +189,7 @@ function AuthScreensInitHandler() { }); PusherConnectionManager.init(); - initializePusher( - session?.accountID, - session?.email, - () => topmostOneTransactionThreadReportIDRef.current, - formatPhoneNumber, - () => reportAttributesRef.current, - ).finally(() => { + initializePusher(session?.accountID, session?.email, () => topmostOneTransactionThreadReportIDRef.current, formatPhoneNumber, () => reportAttributesRef.current).finally(() => { endSpan(CONST.TELEMETRY.SPAN_NAVIGATION.PUSHER_INIT); }); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 89bd0fbaee47..2c8630740931 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1566,12 +1566,14 @@ function getChildTransactions(transactions: OnyxCollection, origina function createUnreportedExpenses(transactions: Array | undefined>): UnreportedExpenseListItemType[] { return transactions .filter((t): t is Transaction => t !== undefined) - .map((transaction): UnreportedExpenseListItemType => ({ - ...transaction, - isDisabled: isTransactionPendingDelete(transaction), - keyForList: transaction.transactionID, - errors: transaction.errors as Errors | undefined, - })); + .map( + (transaction): UnreportedExpenseListItemType => ({ + ...transaction, + isDisabled: isTransactionPendingDelete(transaction), + keyForList: transaction.transactionID, + errors: transaction.errors as Errors | undefined, + }), + ); } type GetEligibleTransactionsToAddParams = { diff --git a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx index c883603c1a6d..c274290c2ff1 100644 --- a/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ComposerWithSuggestions.tsx @@ -956,15 +956,18 @@ function ComposerWithSuggestions({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useImperativeHandle(ref, (): ComposerWithSuggestionsRef => ({ - focus, - replaceSelectionWithText, - getCurrentText, - clearWorklet, - resetHeight, - blur: () => composerRef.current?.blur(), - isFocused: () => composerRef.current?.isFocused() ?? false, - })); + useImperativeHandle( + ref, + (): ComposerWithSuggestionsRef => ({ + focus, + replaceSelectionWithText, + getCurrentText, + clearWorklet, + resetHeight, + blur: () => composerRef.current?.blur(), + isFocused: () => composerRef.current?.isFocused() ?? false, + }), + ); const onClear = useCallback( (textOnClear: string) => {