Skip to content

Commit 49ce089

Browse files
authored
Merge pull request Expensify#96284 from Expensify/vit/suppress-promo-modals-supportal
Suppress promo/training/onboarding modals during supportal sessions
2 parents c0a711b + bd82470 commit 49ce089

24 files changed

Lines changed: 266 additions & 41 deletions

src/components/ProductTrainingContext/index.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
66
import useLocalize from '@hooks/useLocalize';
77
import useOnyx from '@hooks/useOnyx';
88
import useResponsiveLayout from '@hooks/useResponsiveLayout';
9+
import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalUI';
910
import useSidePanelState from '@hooks/useSidePanelState';
1011
import useTheme from '@hooks/useTheme';
1112
import useThemeStyles from '@hooks/useThemeStyles';
@@ -20,7 +21,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
2021
import type ChildrenProps from '@src/types/utils/ChildrenProps';
2122
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
2223

23-
import {isActingAsDelegateSelector} from '@selectors/Account';
2424
import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding';
2525
import {emailSelector} from '@selectors/Session';
2626
import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
@@ -59,7 +59,7 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
5959

6060
const [allPolicies, allPoliciesMetadata] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
6161
const [currentUserLogin, currentUserLoginMetadata] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
62-
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector});
62+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
6363

6464
const isUserPolicyEmployee = useMemo(() => {
6565
if (!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) {
@@ -198,8 +198,8 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
198198

199199
const shouldRenderTooltip = useCallback(
200200
(tooltipName: ProductTrainingTooltipName) => {
201-
// If the user is acting as a copilot, don't show any tooltips
202-
if (isActingAsDelegate) {
201+
// Supportal agents and copilots should not see product-training tooltips on behalf of another account.
202+
if (shouldSuppressPromotionalUI) {
203203
return false;
204204
}
205205
// First check base conditions
@@ -216,7 +216,7 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
216216

217217
return false;
218218
},
219-
[isActingAsDelegate, shouldTooltipBeVisible, determineVisibleTooltip],
219+
[shouldSuppressPromotionalUI, shouldTooltipBeVisible, determineVisibleTooltip],
220220
);
221221

222222
const contextValue = useMemo(

src/hooks/useAIFeaturesPromoModal.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,26 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
1515

1616
import type {OnyxEntry} from 'react-native-onyx';
1717

18-
import {isActingAsDelegateSelector} from '@selectors/Account';
1918
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@selectors/Onboarding';
2019
import {useEffect, useRef} from 'react';
2120

2221
import useOnyx from './useOnyx';
22+
import useShouldSuppressPromotionalUI from './useShouldSuppressPromotionalUI';
2323

2424
let hasRedirectedToAIFeaturesPromoModal = false;
2525
let observedActiveMigrationModalThisSession = false;
2626
let observedActiveOnboardingThisSession = false;
2727

2828
/**
2929
* Hook that navigates to the AI features promo modal if:
30-
* - The user is not acting as a delegate; and
30+
* - The user is not in a supportal or copilot session; and
3131
* - The user has not dismissed the AI features promo modal; and
3232
* - The user has seen neither the migrated user welcome modal nor the onboarding modal in this session
3333
*/
3434
function useAIFeaturesPromoModal(session: OnyxEntry<Session>) {
3535
const [isLoadingApp = true, isLoadingAppMetadata] = useOnyx(ONYXKEYS.IS_LOADING_APP);
36-
const [isActingAsDelegate, accountMetadata] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector});
36+
// Suppresses the promo for supportal/copilot sessions and fails closed while SESSION/ACCOUNT load, so eligibility already waits for ACCOUNT before scheduling the promo
37+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
3738
const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING);
3839
const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector});
3940
const [onboarding, onboardingMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
@@ -67,13 +68,13 @@ function useAIFeaturesPromoModal(session: OnyxEntry<Session>) {
6768
observedActiveOnboardingThisSession = true;
6869
}, [hasCompletedOnboarding]);
6970

70-
const isAllOnyxLoaded = !isLoadingOnyxValue(isLoadingAppMetadata, accountMetadata, dismissedProductTrainingMetadata, tryNewDotMetadata, onboardingMetadata);
71+
const isAllOnyxLoaded = !isLoadingOnyxValue(isLoadingAppMetadata, dismissedProductTrainingMetadata, tryNewDotMetadata, onboardingMetadata);
7172

7273
const isEligible =
7374
isAllOnyxLoaded &&
7475
!!session?.authToken &&
7576
!isLoadingApp &&
76-
!isActingAsDelegate &&
77+
!shouldSuppressPromotionalUI &&
7778
!hasRedirectedToAIFeaturesPromoModal &&
7879
!isAIPromoModalDismissed &&
7980
!isMigrationModalPending &&

src/hooks/useHoldRejectActions.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {useMemoizedLazyExpensifyIcons} from './useLazyAsset';
2121
import useLocalize from './useLocalize';
2222
import useNetwork from './useNetwork';
2323
import useOnyx from './useOnyx';
24+
import useShouldSuppressPromotionalUI from './useShouldSuppressPromotionalUI';
2425

2526
type UseHoldRejectActionsParams = {
2627
reportID: string | undefined;
@@ -54,6 +55,7 @@ function useHoldRejectActions({reportID, onHoldEducationalOpen, onRejectModalOpe
5455
const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION);
5556
const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION);
5657
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
58+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
5759

5860
const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport);
5961
const isChatReportDM = isDM(chatReport);
@@ -76,7 +78,7 @@ function useHoldRejectActions({reportID, onHoldEducationalOpen, onRejectModalOpe
7678

7779
const isDismissed = isReportSubmitter ? dismissedHoldUseExplanation : dismissedRejectUseExplanation;
7880

79-
if (isDismissed || isChatReportDM) {
81+
if (isDismissed || isChatReportDM || shouldSuppressPromotionalUI) {
8082
changeMoneyRequestHoldStatus(requestParentReportAction, transaction, isOffline, currentUserLogin ?? '', currentUserAccountID, transactionViolations, isTrackIntentUser);
8183
} else if (isReportSubmitter) {
8284
onHoldEducationalOpen();
@@ -118,7 +120,7 @@ function useHoldRejectActions({reportID, onHoldEducationalOpen, onRejectModalOpe
118120
return;
119121
}
120122

121-
if (dismissedRejectUseExplanation) {
123+
if (dismissedRejectUseExplanation || shouldSuppressPromotionalUI) {
122124
Navigation.navigate(ROUTES.REJECT_EXPENSE_REPORT.getRoute(moneyRequestReport.reportID));
123125
} else {
124126
onRejectModalOpen(CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.REJECT_REPORT);

src/hooks/useOnboardingFlow.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {emailSelector} from '@selectors/Session';
1717
import {useEffect} from 'react';
1818

1919
import useOnyx from './useOnyx';
20+
import useShouldSuppressPromotionalUI from './useShouldSuppressPromotionalUI';
2021

2122
/**
2223
* Hook to handle redirection to the onboarding flow based on the user's onboarding status
@@ -26,6 +27,7 @@ import useOnyx from './useOnyx';
2627
function useOnboardingFlowRouter() {
2728
const currentUrl = getCurrentUrl();
2829
const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP);
30+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
2931
const [onboardingValues, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
3032
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
3133
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
@@ -94,7 +96,7 @@ function useOnboardingFlowRouter() {
9496
const isMigratedUser = hasBeenAddedToNudgeMigration ?? false;
9597
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
9698
const isInvitedOrGroupMember = (hasNonPersonalPolicy || wasInvitedToNewDot) ?? false;
97-
if (isMigratedUser || isInvitedOrGroupMember) {
99+
if (isMigratedUser || isInvitedOrGroupMember || shouldSuppressPromotionalUI) {
98100
return;
99101
}
100102

@@ -144,6 +146,7 @@ function useOnboardingFlowRouter() {
144146
hasNonPersonalPolicy,
145147
wasInvitedToNewDot,
146148
isOnboardingCompleted,
149+
shouldSuppressPromotionalUI,
147150
isVisitingSecureLink,
148151
]);
149152

src/hooks/useProactiveAppReview.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import CONST from '@src/CONST';
21
import ONYXKEYS from '@src/ONYXKEYS';
3-
import type {Session} from '@src/types/onyx';
42
import type ProactiveAppReview from '@src/types/onyx/AppReview';
53

6-
import type {OnyxEntry} from 'react-native-onyx';
7-
8-
import {isActingAsDelegateSelector} from '@selectors/Account';
94
import {useState} from 'react';
105

116
import useOnyx from './useOnyx';
7+
import useShouldSuppressPromotionalUI from './useShouldSuppressPromotionalUI';
128

139
const THIRTY_DAYS_IN_MS = 30 * 24 * 60 * 60 * 1000;
1410

15-
const authTokenTypeSelector = (session: OnyxEntry<Session>) => session?.authTokenType;
16-
1711
type UseProactiveAppReviewReturn = {
1812
/** Whether the modal should be shown */
1913
shouldShowModal: boolean;
@@ -27,14 +21,13 @@ type UseProactiveAppReviewReturn = {
2721
*/
2822
function useProactiveAppReview(): UseProactiveAppReviewReturn {
2923
const [proactiveAppReview] = useOnyx(ONYXKEYS.NVP_APP_REVIEW);
30-
const [authTokenType] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector});
31-
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector});
24+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
3225

3326
// Capture once so render stays pure (Date.now is impure). Fine for a 30-day cool-down gate.
3427
const [timeAtMount] = useState(Date.now);
3528

3629
let shouldShowModal = true;
37-
if (authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT || isActingAsDelegate) {
30+
if (shouldSuppressPromotionalUI) {
3831
// Supportal agents and copilots should not leave reviews on behalf of another account.
3932
shouldShowModal = false;
4033
} else if (!proactiveAppReview?.trigger) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import ONYXKEYS from '@src/ONYXKEYS';
2+
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
3+
4+
import {isActingAsDelegateSelector} from '@selectors/Account';
5+
import {isSupportalSessionSelector} from '@selectors/Session';
6+
7+
import useOnyx from './useOnyx';
8+
9+
/**
10+
* Returns true when promo, training, and onboarding UI should be hidden (supportal or copilot session).
11+
* Each subscription is narrowed to the derived boolean so consumers only re-render when suppression actually changes.
12+
* Fails closed: while SESSION or ACCOUNT is still loading we suppress, so a supportal/copilot session is never briefly
13+
* treated as a normal one (e.g. a copilot could otherwise be reset into onboarding before delegatedAccess.delegate loads).
14+
*/
15+
function useShouldSuppressPromotionalUI(): boolean {
16+
const [isSupportalSession = false, sessionMetadata] = useOnyx(ONYXKEYS.SESSION, {selector: isSupportalSessionSelector});
17+
const [isActingAsDelegate = false, accountMetadata] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector});
18+
19+
if (isLoadingOnyxValue(sessionMetadata, accountMetadata)) {
20+
return true;
21+
}
22+
23+
return isSupportalSession || isActingAsDelegate;
24+
}
25+
26+
export default useShouldSuppressPromotionalUI;

src/libs/Navigation/AppNavigator/AuthScreens.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import WideRHPContextProvider from '@components/WideRHPContextProvider';
2222

2323
import useOnboardingFlowRouter from '@hooks/useOnboardingFlow';
2424
import useResponsiveLayout from '@hooks/useResponsiveLayout';
25+
import useShouldSuppressPromotionalUI from '@hooks/useShouldSuppressPromotionalUI';
2526
import {SidebarOrderedReportsContextProvider} from '@hooks/useSidebarOrderedReports';
2627
import useStyleUtils from '@hooks/useStyleUtils';
2728
import useTheme from '@hooks/useTheme';
@@ -132,6 +133,7 @@ function AuthScreens() {
132133
const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
133134
const modalCardStyleInterpolator = useModalCardStyleInterpolator();
134135
const {isOnboardingCompleted} = useOnboardingFlowRouter();
136+
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
135137

136138
useEffect(() => {
137139
NavBarManager.setButtonStyle(theme.navigationBarButtonsStyle);
@@ -335,7 +337,7 @@ function AuthScreens() {
335337
component={FeatureTrainingModalNavigator}
336338
listeners={modalScreenListeners}
337339
/>
338-
{isOnboardingCompleted === false && !Navigation.isValidateLoginFlow() && (
340+
{isOnboardingCompleted === false && !shouldSuppressPromotionalUI && !Navigation.isValidateLoginFlow() && (
339341
<RootStack.Screen
340342
name={NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR}
341343
options={{...rootNavigatorScreenOptions.basicModalNavigator, gestureEnabled: false}}

src/libs/Navigation/guards/MigratedUserWelcomeModalGuard.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {OnyxEntry} from 'react-native-onyx';
1616

1717
import {findFocusedRoute} from '@react-navigation/native';
1818
import {tryNewDotOnyxSelector} from '@selectors/Onboarding';
19+
import {isSupportalSessionSelector} from '@selectors/Session';
1920
import Onyx from 'react-native-onyx';
2021

2122
import type {GuardResult, NavigationGuard} from './types';
@@ -44,6 +45,7 @@ function resetSessionFlag() {
4445
*/
4546
function navigateToMigratedUserWelcomeModalIfReady() {
4647
if (
48+
isSupportalSessionSelector(session) ||
4749
!session?.authToken ||
4850
isLoadingApp ||
4951
hasRedirectedToMigratedUserModal ||
@@ -144,6 +146,10 @@ const MigratedUserWelcomeModalGuard: NavigationGuard = {
144146
}
145147

146148
if (hasBeenAddedToNudgeMigration && !isProductTrainingElementDismissed('migratedUserWelcomeModal', dismissedProductTraining)) {
149+
if (context.isSupportalSession) {
150+
return {type: 'ALLOW'};
151+
}
152+
147153
Log.info('[MigratedUserWelcomeModalGuard] Redirecting to migrated user welcome modal');
148154
hasRedirectedToMigratedUserModal = true;
149155

src/libs/Navigation/guards/OnboardingGuard.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {OnyxEntry} from 'react-native-onyx';
1717
import type {ValueOf} from 'type-fest';
1818

1919
import {findFocusedRoute} from '@react-navigation/native';
20+
import {isActingAsDelegateSelector} from '@selectors/Account';
2021
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
2122
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector, wasInvitedToNewDotSelector} from '@selectors/Onboarding';
2223
import Onyx from 'react-native-onyx';
@@ -198,7 +199,10 @@ const OnboardingGuard: NavigationGuard = {
198199
isInvitedOrGroupMember ||
199200
isSingleEntry ||
200201
isFirstTimeHybridAppTransition ||
201-
isNavigatingWithReplace;
202+
isNavigatingWithReplace ||
203+
context.isSupportalSession ||
204+
// Copilots should not be pushed through onboarding on behalf of the account they are accessing
205+
isActingAsDelegateSelector(account);
202206

203207
if (shouldSkipOnboarding) {
204208
return {type: 'ALLOW'};

src/libs/Navigation/guards/SubmitPlanWelcomeModalGuard.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
1919

2020
import {findFocusedRoute} from '@react-navigation/native';
2121
import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding';
22+
import {isSupportalSessionSelector} from '@selectors/Session';
2223
import {Str} from 'expensify-common';
2324
import Onyx from 'react-native-onyx';
2425

@@ -116,7 +117,15 @@ function isPolicyCreationRestricted(): boolean {
116117
* race condition where the modal would re-appear on app restart.
117118
*/
118119
function navigateToSubmitPlanWelcomeModalIfReady() {
119-
if (!session?.authToken || isLoadingApp || !hasLoadedApp || hasRedirectedToSubmitPlanModal || !isSubmitMigrationModalShownLoaded || !shouldShowSubmitPlanWelcomeModal()) {
120+
if (
121+
isSupportalSessionSelector(session) ||
122+
!session?.authToken ||
123+
isLoadingApp ||
124+
!hasLoadedApp ||
125+
hasRedirectedToSubmitPlanModal ||
126+
!isSubmitMigrationModalShownLoaded ||
127+
!shouldShowSubmitPlanWelcomeModal()
128+
) {
120129
return;
121130
}
122131

@@ -294,16 +303,16 @@ const SubmitPlanWelcomeModalGuard: NavigationGuard = {
294303
return {type: 'ALLOW'};
295304
}
296305

297-
if (shouldShowSubmitPlanWelcomeModal()) {
298-
hasRedirectedToSubmitPlanModal = true;
299-
300-
return {
301-
type: 'REDIRECT',
302-
route: getSubmitPlanWelcomeModalRoute(),
303-
};
306+
if (context.isSupportalSession || !shouldShowSubmitPlanWelcomeModal()) {
307+
return {type: 'ALLOW'};
304308
}
305309

306-
return {type: 'ALLOW'};
310+
hasRedirectedToSubmitPlanModal = true;
311+
312+
return {
313+
type: 'REDIRECT',
314+
route: getSubmitPlanWelcomeModalRoute(),
315+
};
307316
},
308317
};
309318

0 commit comments

Comments
 (0)