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..c2c3be5c906c --- /dev/null +++ b/src/pages/settings/HelpPage/TalkToHumanPage.tsx @@ -0,0 +1,56 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; + +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'; + +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; + + // 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. + // 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. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOffline]); + + return ; +} + +TalkToHumanPage.displayName = 'TalkToHumanPage'; + +export default TalkToHumanPage;