Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
60ba12a
feat: ai feature promo modal navigation, basic component and guards
gijoe0295 May 28, 2026
3b61e1e
add English terms
gijoe0295 May 28, 2026
b6493ea
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 1, 2026
847a4fb
run translation script
gijoe0295 Jun 1, 2026
dc88496
remove deprecated getUrlWithBackToParam
gijoe0295 Jun 2, 2026
b6d3d51
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 8, 2026
e9738ef
improve navigation guard to account for explanation modal
gijoe0295 Jun 8, 2026
d78c768
add lottie files
gijoe0295 Jun 8, 2026
07bdc5a
implement AI features promo modal UI
gijoe0295 Jun 8, 2026
b2753a6
updated translations
gijoe0295 Jun 8, 2026
d8a8c15
fix lint
gijoe0295 Jun 8, 2026
fe201cf
fix lint
gijoe0295 Jun 8, 2026
59c7595
use FlatList to work on native
gijoe0295 Jun 8, 2026
59cdaa3
updated lotties
gijoe0295 Jun 8, 2026
a803989
add dismiss icons and pagination dots
gijoe0295 Jun 8, 2026
6317720
pre-dismiss AI promo modal NVP
gijoe0295 Jun 8, 2026
77cdff4
fix lint and test
gijoe0295 Jun 8, 2026
f37d6c8
fix failed tests
gijoe0295 Jun 8, 2026
a70c127
attach ai modal guard to navigation state instead
gijoe0295 Jun 8, 2026
7ae14b3
add Beta Badge
gijoe0295 Jun 8, 2026
5d0393f
address feedbacks
gijoe0295 Jun 8, 2026
0fbe423
handle cases where onboarding value is loaded after promo modal guard
gijoe0295 Jun 8, 2026
5b39f3e
keep Beta badge closer
gijoe0295 Jun 9, 2026
d669235
keep content still, scroll illustration only
gijoe0295 Jun 9, 2026
40d6f5e
change pagination and dismiss icon color
gijoe0295 Jun 9, 2026
a5454be
only play animation when focused
gijoe0295 Jun 9, 2026
ccdb43a
modal height changes
gijoe0295 Jun 10, 2026
da60bc4
only play animation when focused
gijoe0295 Jun 10, 2026
88736cd
fix broken layout on native and do not animate pagination dots
gijoe0295 Jun 10, 2026
9fb7697
replace new lottie
gijoe0295 Jun 10, 2026
e879051
enforce onboarding completed check
gijoe0295 Jun 10, 2026
bd33c99
remove unused exports
gijoe0295 Jun 10, 2026
d266511
fix lint
gijoe0295 Jun 10, 2026
a0b0037
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 10, 2026
8627cb8
replace new lottie
gijoe0295 Jun 11, 2026
814d8e9
update lint seatbelt to fix lint because we just renamed the files
gijoe0295 Jun 11, 2026
7edb45e
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 12, 2026
a0a21f6
replaced lottie
gijoe0295 Jun 12, 2026
7ca8c4d
add a min height so modal height persists accross pages
gijoe0295 Jun 15, 2026
de8b4a9
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 15, 2026
82d3b97
exclude ai promo modal from last visted path so it will not appear ag…
gijoe0295 Jun 15, 2026
198fcb3
auto play Section lottie
gijoe0295 Jun 15, 2026
0526781
fix: width is applied on narrow layout
gijoe0295 Jun 15, 2026
5eb9a52
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 16, 2026
14e4d19
pre-render text content to lock modal height to the tallest page
gijoe0295 Jun 16, 2026
e4ea212
fix: Android crash because modal is showing too early
gijoe0295 Jun 16, 2026
24a9c1b
uncomment the nvp guard
gijoe0295 Jun 17, 2026
82e0cd9
update the build agents help link
gijoe0295 Jun 17, 2026
5a3137d
Merge branch 'main' of github.com:gijoe0295/App into feat/ai-feature-…
gijoe0295 Jun 17, 2026
bcdd2f7
fix typecheck
gijoe0295 Jun 17, 2026
66e1632
fix lint
gijoe0295 Jun 17, 2026
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
Binary file added assets/animations/CustomAgents.lottie
Binary file not shown.
Binary file added assets/animations/ExpenseAssistant.lottie
Binary file not shown.
Binary file added assets/animations/SpendAnalysis.lottie
Binary file not shown.
6 changes: 4 additions & 2 deletions config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@
"../../src/components/EmptySelectionListContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/EnvironmentBadge.tsx" "no-restricted-syntax" 1
"../../src/components/ExplanationModal.tsx" "no-restricted-syntax" 1
"../../src/components/FeatureTrainingModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/FeatureTrainingModal.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/FeatureTrainingModal/FeatureTrainingModalIllustration.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/FeatureTrainingModal/FeatureTrainingModalIllustration.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/FeatureTrainingModal/index.tsx" "no-restricted-imports" 1
"../../src/components/FeedbackSurvey.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/FilePicker/index.native.tsx" "react-hooks/refs" 1
"../../src/components/FilePicker/index.tsx" "react-hooks/refs" 1
Expand Down Expand Up @@ -671,6 +672,7 @@
"../../src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper/index.native.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/Navigation/PlatformStackNavigation/navigationOptions/animation/withAnimation.ts" "@typescript-eslint/no-unsafe-type-assertion" 6
"../../src/libs/Navigation/guards/AIFeaturesPromoGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/guards/MigratedUserWelcomeModalGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/libs/Navigation/guards/OnboardingGuard.ts" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/libs/Navigation/helpers/createNormalizedConfigs.ts" "@typescript-eslint/no-deprecated/escape" 1
Expand Down
10 changes: 10 additions & 0 deletions jest/setupAfterEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import ONYXKEYS from '@src/ONYXKEYS';

jest.useRealTimers();

// Globally short-circuit AIFeaturesPromoGuard in tests. The real guard proactively
// navigates any authenticated session to /ai-features-promo unless the dismissal NVP
// is set, which would otherwise intercept navigation in unrelated UI tests. Tests
// that need the real guard can override this mock locally.
jest.mock('@libs/Navigation/guards/AIFeaturesPromoGuard', () => ({
__esModule: true,
default: {name: 'AIFeaturesPromoGuard', evaluate: () => ({type: 'ALLOW'})},
onSessionOrLoadingAppChanged: jest.fn(),
}));

// Patch Keyboard.addListener to return a subscription object with .remove() so that
// @react-navigation/bottom-tabs useIsKeyboardShown hook doesn't crash on cleanup.
if (Keyboard && typeof Keyboard.addListener === 'function') {
Expand Down
9 changes: 9 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7248,6 +7248,7 @@ const CONST = {
SCREENS.SAML_SIGN_IN,
SCREENS.VALIDATE_LOGIN,
SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT,
SCREENS.AI_FEATURES_PROMO_MODAL.ROOT,
SCREENS.MONEY_REQUEST.STEP_SCAN,
SCREENS.DOMAIN.MEMBERS_MOVE_TO_GROUP,
...Object.values(SCREENS.MULTIFACTOR_AUTHENTICATION),
Expand Down Expand Up @@ -7766,6 +7767,14 @@ const CONST = {

MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal',

AI_FEATURES_PROMO_MODAL: 'aiFeaturesPromoModal',

AI_FEATURES_PROMO_LEARN_MORE_URLS: {
SPEND_ANALYSIS: 'https://help.expensify.com/articles/new-expensify/concierge-ai/How-Concierge-Analyzes-Spend',
EXPENSE_ASSISTANT: 'https://help.expensify.com/articles/new-expensify/concierge-ai/Expense-Assistant',
BUILD_AGENTS: 'https://help.expensify.com/articles/new-expensify/ai-agents/Create-Agent-Rules',
},

BASE_LIST_ITEM_TEST_ID: 'base-list-item-',
SELECTION_BUTTON_TEST_ID: 'selection-button-',
PRODUCT_TRAINING_TOOLTIP_NAMES: {
Expand Down
1 change: 1 addition & 0 deletions src/NAVIGATORS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator',
FEATURE_TRAINING_MODAL_NAVIGATOR: 'FeatureTrainingModalNavigator',
MIGRATED_USER_MODAL_NAVIGATOR: 'MigratedUserModalNavigator',
AI_FEATURES_PROMO_MODAL_NAVIGATOR: 'AIFeaturesPromoModalNavigator',
TEST_DRIVE_MODAL_NAVIGATOR: 'TestDriveModalNavigator',
TEST_DRIVE_DEMO_NAVIGATOR: 'TestDriveDemoNavigator',
REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator',
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3367,6 +3367,7 @@ const ROUTES = {

getRoute: (backTo?: string) => getUrlWithBackToParam('onboarding/migrated-user-welcome', backTo, false),
},
AI_FEATURES_PROMO_MODAL: 'ai-features-promo',

TRANSACTION_RECEIPT: {
route: 'r/:reportID/transaction/:transactionID/receipt/:action?/:iouType?',
Expand Down
4 changes: 4 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,10 @@ const SCREENS = {
ROOT: 'MigratedUserWelcomeModal_Root',
},

AI_FEATURES_PROMO_MODAL: {
ROOT: 'AIFeaturesPromoModal_Root',
},

TEST_DRIVE_MODAL: {
ROOT: 'TestDrive_Modal_Root',
},
Expand Down
92 changes: 92 additions & 0 deletions src/components/AIFeaturesPromoModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
import Badge from '@components/Badge';
import FeatureTrainingModal from '@components/FeatureTrainingModal';
import type {FeatureTrainingModalPageProps} from '@components/FeatureTrainingModal';
import LottieAnimations from '@components/LottieAnimations';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import {dismissProductTraining} from '@libs/actions/Welcome';
import Log from '@libs/Log';
import variables from '@styles/variables';
import CONST from '@src/CONST';

function AIFeaturesPromoModal() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {isBetaEnabled} = usePermissions();
const canUseCustomAgent = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT);

const customAgentPromoTitle = (
<View style={[styles.dFlex, styles.flexRow]}>
<Text style={[styles.textHeadlineH1, styles.mb2]}>{translate('aiFeaturesPromoModal.customAgents.title')}</Text>
<Badge
isStrong
isCondensed
text={translate('common.beta')}
badgeStyles={styles.mb2}
/>
</View>
);

const pages: FeatureTrainingModalPageProps[] = [
{
animation: LottieAnimations.SpendAnalysis,
title: translate('aiFeaturesPromoModal.spendAnalysis.title'),
subtitle: translate('aiFeaturesPromoModal.subtitle'),
description: translate('aiFeaturesPromoModal.spendAnalysis.description'),
confirmText: translate('common.next'),
},
{
animation: LottieAnimations.ExpenseAssistant,
title: translate('aiFeaturesPromoModal.expenseAssistant.title'),
subtitle: translate('aiFeaturesPromoModal.subtitle'),
description: translate('aiFeaturesPromoModal.expenseAssistant.description'),
confirmText: canUseCustomAgent ? translate('common.next') : translate('aiFeaturesPromoModal.confirmText'),
},
...(canUseCustomAgent
? [
{
animation: LottieAnimations.CustomAgents,
title: customAgentPromoTitle,
subtitle: translate('aiFeaturesPromoModal.subtitle'),
description: translate('aiFeaturesPromoModal.customAgents.description'),
confirmText: translate('aiFeaturesPromoModal.confirmText'),
},
]
: []),
];

const wasDismissedViaConfirmRef = useRef(false);

const onConfirm = () => {
Log.hmmm('[AIFeaturesPromoModal] onConfirm called, recording click dismissal');
wasDismissedViaConfirmRef.current = true;
};

const onClose = () => {
const isCloseButtonDismissal = !wasDismissedViaConfirmRef.current;
Log.hmmm(`[AIFeaturesPromoModal] onClose called, dismissing product training via ${isCloseButtonDismissal ? 'x' : 'click'}`);
dismissProductTraining(CONST.AI_FEATURES_PROMO_MODAL, isCloseButtonDismissal);
};

return (
<FeatureTrainingModal
pages={pages}
onConfirm={onConfirm}
onClose={onClose}
Comment thread
gijoe0295 marked this conversation as resolved.
width={variables.aiFeaturesPromoModalWidth}
shouldRenderHTMLDescription
shouldUseScrollView
illustrationOuterContainerStyle={styles.p0}
illustrationAspectRatio={LottieAnimations.SpendAnalysis.w / LottieAnimations.SpendAnalysis.h}
contentInnerContainerStyles={styles.mb4}
modalInnerContainerStyle={styles.pt0}
titleStyles={styles.mb2}
/>
);
}

export default AIFeaturesPromoModal;
110 changes: 110 additions & 0 deletions src/components/FeatureTrainingModal/FeatureTrainingModalBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import {View} from 'react-native';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import FeatureTrainingModalContent from './FeatureTrainingModalContent';
import FeatureTrainingModalIllustration from './FeatureTrainingModalIllustration';
import type {BaseFeatureTrainingModalProps, FeatureTrainingModalPageProps} from './index';

type FeatureTrainingModalBodyProps = BaseFeatureTrainingModalProps &
FeatureTrainingModalPageProps & {
/** Padding for the modal */
modalPadding: number;

/** Whether the modal should be shown again */
willShowAgain: boolean;

/** A callback to call when the modal should be shown again */
toggleWillShowAgain: () => void;

/** A callback to call when we want to close the modal */
closeModal: (didPressHelpButton?: boolean) => void;

/** A callback to call when we want to close the modal and confirm */
confirmModal: () => void;

/** Whether to show the back button to navigate back to the previous page in carousel mode */
shouldShowBackButton?: boolean;

/** A callback to call when we want to navigate back to the previous page in carousel mode */
onBack?: () => void;
};

function FeatureTrainingModalBody({
illustrationInnerContainerStyle,
illustrationOuterContainerStyle,
illustrationAspectRatio: illustrationAspectRatioProp,
width,
title = '',
subtitle = '',
description = '',
secondaryDescription = '',
titleStyles,
shouldShowDismissModalOption = false,
confirmText = '',
helpText = '',
onHelp = () => {},
children,
contentInnerContainerStyles,
contentOuterContainerStyles,
shouldRenderSVG = true,
shouldRenderHTMLDescription = false,
shouldShowConfirmationLoader = false,
canConfirmWhileOffline = true,
shouldCallOnHelpWhenModalHidden = false,
helpSentryLabel,
confirmSentryLabel,
modalPadding,
willShowAgain = true,
toggleWillShowAgain,
closeModal,
confirmModal,
shouldShowBackButton = false,
onBack,
...props
}: FeatureTrainingModalBodyProps) {
const StyleUtils = useStyleUtils();
const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();

return (
<View style={width && onboardingIsMediumOrLargerScreenWidth ? StyleUtils.getWidthStyle(width) : undefined}>
<FeatureTrainingModalIllustration
illustrationAspectRatio={illustrationAspectRatioProp}
illustrationInnerContainerStyle={illustrationInnerContainerStyle}
illustrationOuterContainerStyle={illustrationOuterContainerStyle}
shouldRenderSVG={shouldRenderSVG}
modalPadding={modalPadding}
{...props}
/>
<FeatureTrainingModalContent
title={title}
subtitle={subtitle}
description={description}
secondaryDescription={secondaryDescription}
confirmText={confirmText}
helpText={helpText}
onHelp={onHelp}
shouldCallOnHelpWhenModalHidden={shouldCallOnHelpWhenModalHidden}
helpSentryLabel={helpSentryLabel}
confirmSentryLabel={confirmSentryLabel}
shouldShowDismissModalOption={shouldShowDismissModalOption}
willShowAgain={willShowAgain}
toggleWillShowAgain={toggleWillShowAgain}
closeModal={closeModal}
confirmModal={confirmModal}
shouldShowBackButton={shouldShowBackButton}
onBack={onBack}
shouldShowConfirmationLoader={shouldShowConfirmationLoader}
canConfirmWhileOffline={canConfirmWhileOffline}
titleStyles={titleStyles}
contentInnerContainerStyles={contentInnerContainerStyles}
contentOuterContainerStyles={contentOuterContainerStyles}
shouldRenderHTMLDescription={shouldRenderHTMLDescription}
>
{children}
</FeatureTrainingModalContent>
</View>
);
}

export default FeatureTrainingModalBody;
Loading
Loading