Skip to content

Add close button to product training tooltips and track dismissal method #58666 #59008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions src/components/ProductTrainingContext/TOOLTIPS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type ShouldShowConditionProps = {

type TooltipData = {
content: Array<{text: TranslationPaths; isBold: boolean}>;
onHideTooltip: () => void;
onHideTooltip: (isDismissedUsingCloseButton?: boolean) => void;
name: ProductTrainingTooltipName;
priority: number;
shouldShow: (props: ShouldShowConditionProps) => boolean;
Expand All @@ -35,7 +35,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false},
{text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true},
],
onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(CONCEIRGE_LHN_GBR, isDismissedUsingCloseButton),
name: CONCEIRGE_LHN_GBR,
priority: 1300,
// TODO: CONCEIRGE_LHN_GBR tooltip will be replaced by a tooltip in the #admins room
Expand All @@ -47,7 +47,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true},
{text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false},
],
onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(RENAME_SAVED_SEARCH, isDismissedUsingCloseButton),
name: RENAME_SAVED_SEARCH,
priority: 1250,
shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout,
Expand All @@ -58,7 +58,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.globalCreateTooltip.part2', isBold: false},
{text: 'productTrainingTooltip.globalCreateTooltip.part3', isBold: false},
],
onHideTooltip: () => dismissProductTraining(GLOBAL_CREATE_TOOLTIP),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(GLOBAL_CREATE_TOOLTIP, isDismissedUsingCloseButton),
name: GLOBAL_CREATE_TOOLTIP,
priority: 1200,
shouldShow: () => true,
Expand All @@ -69,7 +69,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.bottomNavInboxTooltip.part2', isBold: false},
{text: 'productTrainingTooltip.bottomNavInboxTooltip.part3', isBold: false},
],
onHideTooltip: () => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP, isDismissedUsingCloseButton),
name: BOTTOM_NAV_INBOX_TOOLTIP,
priority: 900,
shouldShow: () => true,
Expand All @@ -80,7 +80,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.workspaceChatTooltip.part2', isBold: false},
{text: 'productTrainingTooltip.workspaceChatTooltip.part3', isBold: false},
],
onHideTooltip: () => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP, isDismissedUsingCloseButton),
name: LHN_WORKSPACE_CHAT_TOOLTIP,
priority: 800,
shouldShow: () => true,
Expand All @@ -102,7 +102,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.scanTestTooltip.part4', isBold: true},
{text: 'productTrainingTooltip.scanTestTooltip.part5', isBold: false},
],
onHideTooltip: () => dismissProductTraining(SCAN_TEST_TOOLTIP_MANAGER),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(SCAN_TEST_TOOLTIP_MANAGER, isDismissedUsingCloseButton),
name: SCAN_TEST_TOOLTIP_MANAGER,
priority: 1000,
shouldShow: () => true,
Expand All @@ -113,7 +113,7 @@ const TOOLTIPS: Record<ProductTrainingTooltipName, TooltipData> = {
{text: 'productTrainingTooltip.scanTestTooltip.part7', isBold: true},
{text: 'productTrainingTooltip.scanTestTooltip.part8', isBold: false},
],
onHideTooltip: () => dismissProductTraining(SCAN_TEST_CONFIRMATION),
onHideTooltip: (isDismissedUsingCloseButton = false) => dismissProductTraining(SCAN_TEST_CONFIRMATION, isDismissedUsingCloseButton),
name: SCAN_TEST_CONFIRMATION,
priority: 1100,
shouldShow: () => true,
Expand Down
82 changes: 56 additions & 26 deletions src/components/ProductTrainingContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -12,6 +13,8 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {parseFSAttributes} from '@libs/Fullstory';
import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
Expand Down Expand Up @@ -98,7 +101,7 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
return false;
}

const isDismissed = !!dismissedProductTraining?.[tooltipName];
const isDismissed = isProductTrainingElementDismissed(tooltipName, dismissedProductTraining);

if (isDismissed) {
return false;
Expand Down Expand Up @@ -202,6 +205,22 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
*/
useLayoutEffect(parseFSAttributes, []);

const shouldShowProductTrainingTooltip = useMemo(() => {
return shouldShow && shouldRenderTooltip(tooltipName) && !shouldHideToolTip;
}, [shouldRenderTooltip, tooltipName, shouldShow, shouldHideToolTip]);

const hideTooltip = useCallback(
(isDismissedUsingCloseButton = false) => {
if (!shouldShowProductTrainingTooltip) {
return;
}
const tooltip = TOOLTIPS[tooltipName];
tooltip.onHideTooltip(isDismissedUsingCloseButton);
unregisterTooltip(tooltipName);
},
[tooltipName, shouldShowProductTrainingTooltip, unregisterTooltip],
);

const renderProductTrainingTooltip = useCallback(() => {
const tooltip = TOOLTIPS[tooltipName];
return (
Expand All @@ -217,7 +236,8 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
styles.flexWrap,
styles.textAlignCenter,
styles.gap3,
styles.p2,
styles.pv2,
styles.ph1,
]}
>
<Icon
Expand All @@ -238,6 +258,23 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
);
})}
</Text>
{!tooltip?.shouldRenderActionButtons && (
<PressableWithoutFeedback
onPress={() => {
hideTooltip(true);
}}
shouldUseAutoHitSlop
accessibilityLabel={translate('productTrainingTooltip.scanTestTooltip.noThanks')}
role={CONST.ROLE.BUTTON}
>
<Icon
src={Expensicons.Close}
fill={theme.icon}
width={variables.iconSizeSemiSmall}
height={variables.iconSizeSemiSmall}
/>
</PressableWithoutFeedback>
)}
</View>
{!!tooltip?.shouldRenderActionButtons && (
<View style={[styles.alignItemsCenter, styles.justifyContentBetween, styles.flexRow, styles.ph2, styles.pv2, styles.gap2]}>
Expand All @@ -257,42 +294,35 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
</View>
);
}, [
config.onConfirm,
config.onDismiss,
tooltipName,
styles.alignItemsCenter,
styles.flex1,
styles.flexRow,
styles.justifyContentStart,
styles.justifyContentCenter,
styles.flexWrap,
styles.textAlignCenter,
styles.gap3,
styles.justifyContentBetween,
styles.justifyContentCenter,
styles.mw100,
styles.p2,
styles.productTrainingTooltipText,
styles.pv2,
styles.textAlignCenter,
styles.textBold,
styles.ph1,
styles.productTrainingTooltipText,
styles.textWrap,
styles.gap2,
styles.justifyContentStart,
styles.mw100,
styles.flex1,
styles.justifyContentBetween,
styles.ph2,
styles.gap2,
styles.textBold,
theme.tooltipHighlightText,
tooltipName,
theme.icon,
translate,
config.onConfirm,
config.onDismiss,
hideTooltip,
]);

const shouldShowProductTrainingTooltip = useMemo(() => {
return shouldShow && shouldRenderTooltip(tooltipName) && !shouldHideToolTip;
}, [shouldRenderTooltip, tooltipName, shouldShow, shouldHideToolTip]);

const hideProductTrainingTooltip = useCallback(() => {
if (!shouldShowProductTrainingTooltip) {
return;
}
const tooltip = TOOLTIPS[tooltipName];
tooltip.onHideTooltip();
unregisterTooltip(tooltipName);
}, [tooltipName, shouldShowProductTrainingTooltip, unregisterTooltip]);
hideTooltip(false);
}, [hideTooltip]);

return {
renderProductTrainingTooltip,
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useOnboardingFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {startOnboardingFlow} from '@libs/actions/Welcome/OnboardingFlow';
import Navigation from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -45,8 +46,7 @@ function useOnboardingFlowRouter() {
if (CONFIG.IS_HYBRID_APP && isLoadingOnyxValue(isSingleNewDotEntryMetadata)) {
return;
}

if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.migratedUserWelcomeModal) {
if (hasBeenAddedToNudgeMigration && !isProductTrainingElementDismissed('migratedUserWelcomeModal', dismissedProductTraining)) {
const defaultCannedQuery = buildCannedSearchQuery();
const query = defaultCannedQuery;
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query}));
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/DismissProductTraining.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type DismissProductTrainingParams = {
name: string;
dismissedMethod: 'x' | 'click';
};

export default DismissProductTrainingParams;
8 changes: 8 additions & 0 deletions src/libs/TooltipUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {OnyxEntry} from 'react-native-onyx';
import type {DismissedProductTraining} from '@src/types/onyx';

function isProductTrainingElementDismissed(elementName: keyof DismissedProductTraining, dismissedProductTraining: OnyxEntry<DismissedProductTraining>) {
return typeof dismissedProductTraining?.[elementName] === 'string' ? !!dismissedProductTraining?.[elementName] : !!dismissedProductTraining?.[elementName]?.timestamp;
}

export default isProductTrainingElementDismissed;
7 changes: 5 additions & 2 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5060,11 +5060,14 @@ function dismissChangePolicyModal() {
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
value: {
[CONST.CHANGE_POLICY_TRAINING_MODAL]: DateUtils.getDBTime(date.valueOf()),
[CONST.CHANGE_POLICY_TRAINING_MODAL]: {
timestamp: DateUtils.getDBTime(date.valueOf()),
dismissedMethod: 'click',
},
},
},
];
API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: CONST.CHANGE_POLICY_TRAINING_MODAL}, {optimisticData});
API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: CONST.CHANGE_POLICY_TRAINING_MODAL, dismissedMethod: 'click'}, {optimisticData});
}

/**
Expand Down
10 changes: 7 additions & 3 deletions src/libs/actions/Welcome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,22 @@ function setSelfTourViewed(shouldUpdateOnyxDataOnlyLocally = false) {
API.write(WRITE_COMMANDS.SELF_TOUR_VIEWED, null, {optimisticData});
}

function dismissProductTraining(elementName: string) {
function dismissProductTraining(elementName: string, isDismissedUsingCloseButton = false) {
const date = new Date();
const dismissedMethod = isDismissedUsingCloseButton ? 'x' : 'click';
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING,
value: {
[elementName]: DateUtils.getDBTime(date.valueOf()),
[elementName]: {
timestamp: DateUtils.getDBTime(date.valueOf()),
dismissedMethod,
},
},
},
];
API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: elementName}, {optimisticData});
API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: elementName, dismissedMethod}, {optimisticData});
}

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ function IOURequestStepScan({
{
onConfirm: setTestReceiptAndNavigate,
onDismiss: () => {
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP);
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, true);
},
},
);
Expand Down
2 changes: 1 addition & 1 deletion src/pages/iou/request/step/IOURequestStepScan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ function IOURequestStepScan({
{
onConfirm: setTestReceiptAndNavigate,
onDismiss: () => {
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP);
dismissProductTraining(CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_TOOLTIP, true);
},
},
);
Expand Down
31 changes: 21 additions & 10 deletions src/types/onyx/DismissedProductTraining.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,61 +10,72 @@ const {
SCAN_TEST_TOOLTIP_MANAGER,
SCAN_TEST_CONFIRMATION,
} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES;

/**
* This type is used to store the timestamp of when the user dismisses a product training ui elements.
*/
type DismissedProductTrainingElement = {
/** The timestamp of when the user dismissed the product training element. */
timestamp: string;

/** The method of how the user dismissed the product training element, click or x. */
dismissedMethod: 'click' | 'x';
};
/**
* This type is used to store the timestamp of when the user dismisses a product training ui elements.
*/
type DismissedProductTraining = {
/**
* When user dismisses the nudgeMigration Welcome Modal, we store the timestamp here.
*/
[CONST.MIGRATED_USER_WELCOME_MODAL]: string;
[CONST.MIGRATED_USER_WELCOME_MODAL]: DismissedProductTrainingElement;

// TODO: CONCEIRGE_LHN_GBR tooltip will be replaced by a tooltip in the #admins room
// https://github.com/Expensify/App/issues/57045#issuecomment-2701455668
/**
* When user dismisses the conciergeLHNGBR product training tooltip, we store the timestamp here.
*/
[CONCEIRGE_LHN_GBR]: string;
[CONCEIRGE_LHN_GBR]: DismissedProductTrainingElement;

/**
* When user dismisses the renameSavedSearch product training tooltip, we store the timestamp here.
*/
[RENAME_SAVED_SEARCH]: string;
[RENAME_SAVED_SEARCH]: DismissedProductTrainingElement;

/**
* When user dismisses the bottomNavInboxTooltip product training tooltip, we store the timestamp here.
*/
[BOTTOM_NAV_INBOX_TOOLTIP]: string;
[BOTTOM_NAV_INBOX_TOOLTIP]: DismissedProductTrainingElement;

/**
* When user dismisses the lhnWorkspaceChatTooltip product training tooltip, we store the timestamp here.
*/
[LHN_WORKSPACE_CHAT_TOOLTIP]: string;
[LHN_WORKSPACE_CHAT_TOOLTIP]: DismissedProductTrainingElement;

/**
* When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
*/
[GLOBAL_CREATE_TOOLTIP]: string;
[GLOBAL_CREATE_TOOLTIP]: DismissedProductTrainingElement;

/**
* When user dismisses the globalCreateTooltip product training tooltip, we store the timestamp here.
*/
[SCAN_TEST_TOOLTIP]: string;
[SCAN_TEST_TOOLTIP]: DismissedProductTrainingElement;

/**
* When user dismisses the test manager tooltip product training tooltip, we store the timestamp here.
*/
[SCAN_TEST_TOOLTIP_MANAGER]: string;
[SCAN_TEST_TOOLTIP_MANAGER]: DismissedProductTrainingElement;

/**
* When user dismisses the test manager on confirmantion page product training tooltip, we store the timestamp here.
*/
[SCAN_TEST_CONFIRMATION]: string;
[SCAN_TEST_CONFIRMATION]: DismissedProductTrainingElement;

/**
* When user dismisses the ChangeReportPolicy feature training modal, we store the timestamp here.
*/
[CONST.CHANGE_POLICY_TRAINING_MODAL]: string;
[CONST.CHANGE_POLICY_TRAINING_MODAL]: DismissedProductTrainingElement;
};

export default DismissedProductTraining;
Loading
Loading