diff --git a/assets/images/envelope-open-star.svg b/assets/images/envelope-open-star.svg
new file mode 100644
index 0000000000000..74652c126f5fe
--- /dev/null
+++ b/assets/images/envelope-open-star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 3088353162f84..bd9ff66b349a6 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -49,6 +49,7 @@ import Download from '@assets/images/download.svg';
import DragAndDrop from '@assets/images/drag-and-drop.svg';
import DragHandles from '@assets/images/drag-handles.svg';
import Emoji from '@assets/images/emoji.svg';
+import EnvelopeOpenStar from '@assets/images/envelope-open-star.svg';
import EReceiptIcon from '@assets/images/eReceiptIcon.svg';
import Exclamation from '@assets/images/exclamation.svg';
import Exit from '@assets/images/exit.svg';
@@ -215,6 +216,7 @@ export {
DragHandles,
EReceiptIcon,
Emoji,
+ EnvelopeOpenStar,
ExpenseCopy,
Exclamation,
Exit,
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 30971f4236f1f..3bd6b211b1553 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -81,6 +81,7 @@ import Emoji from '@assets/images/emoji.svg';
import Lightbulb from '@assets/images/emojiCategoryIcons/light-bulb.svg';
import EmptyStateRoutePending from '@assets/images/emptystate__routepending.svg';
import EmptyStateSpyPigeon from '@assets/images/emptystate__spy-pigeon.svg';
+import EnvelopeOpenStar from '@assets/images/envelope-open-star.svg';
import EReceiptIcon from '@assets/images/eReceiptIcon.svg';
import Exclamation from '@assets/images/exclamation.svg';
import Exit from '@assets/images/exit.svg';
@@ -319,6 +320,7 @@ const Expensicons = {
DragHandles,
EReceiptIcon,
Emoji,
+ EnvelopeOpenStar,
EmptyStateRoutePending,
ExpenseCopy,
Exclamation,
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 479532f059cd6..56eef0b683adf 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -986,6 +986,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Verbindung der persönlichen Karte ${cardName} reparieren` : 'Verbindung der persönlichen Karte reparieren'),
subtitle: 'Wallet',
},
+ validateAccount: {title: 'Bestätigen Sie Ihr Konto, um Expensify weiter zu verwenden', subtitle: 'Konto', cta: 'Bestätigen'},
},
assignedCards: 'Ihre Expensify Karten',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 57d99ff580a70..9e1b3c985c71b 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1019,6 +1019,11 @@ const translations = {
subtitle: 'Expensify Card',
cta: 'Review',
},
+ validateAccount: {
+ title: 'Validate your account to continue using Expensify',
+ subtitle: 'Account',
+ cta: 'Validate',
+ },
},
assignedCards: 'Your Expensify Cards',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} remaining`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 8175dfea22dc0..28c768351a01a 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -891,6 +891,11 @@ const translations: TranslationDeepObject = {
subtitle: 'Tarjeta Expensify',
cta: 'Revisar',
},
+ validateAccount: {
+ title: 'Valida tu cuenta para continuar usando Expensify',
+ subtitle: 'Cuenta',
+ cta: 'Validar',
+ },
},
assignedCards: 'Tus tarjetas Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restantes`,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 3c5ff04c4e890..e3141cb9aa261 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -989,6 +989,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Réparer la connexion de la carte personnelle ${cardName}` : 'Corriger la connexion de la carte personnelle'),
subtitle: 'Portefeuille',
},
+ validateAccount: {title: 'Validez votre compte pour continuer à utiliser Expensify', subtitle: 'Compte', cta: 'Valider'},
},
assignedCards: 'Vos cartes Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restant`,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index a14b9bf77a55c..2814a3f55957f 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -986,6 +986,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Correggi la connessione della carta personale ${cardName}` : 'Correggi connessione carta personale'),
subtitle: 'Portafoglio',
},
+ validateAccount: {title: 'Conferma il tuo account per continuare a usare Expensify', subtitle: 'Account', cta: 'Conferma'},
},
assignedCards: 'Le tue Carte Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} rimanenti`,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index d5bdfadc8efe5..1fb0fb5313e16 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -978,6 +978,7 @@ const translations: TranslationDeepObject = {
subtitle: ({policyName}: {policyName: string}) => `${policyName} > 会計`,
},
fixPersonalCardConnection: {title: ({cardName}: {cardName?: string}) => (cardName ? `${cardName}個人カードの接続を修正` : '個人カードの連携を修正'), subtitle: 'ウォレット'},
+ validateAccount: {title: 'Expensify を引き続きご利用いただくには、アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'},
},
assignedCards: 'お客様の Expensify カード',
assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index d43530ba86d30..5bee400ffe6f5 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -985,6 +985,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Verbinding van persoonlijke kaart ${cardName} herstellen` : 'Verbinding persoonlijke kaart herstellen'),
subtitle: 'Portemonnee',
},
+ validateAccount: {title: 'Valideer je account om Expensify te blijven gebruiken', subtitle: 'Account', cta: 'Valideren'},
},
assignedCards: 'Je Expensify Kaarten',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 9afc913a06cc5..3bea0ab6b5ffd 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -986,6 +986,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Napraw połączenie z prywatną kartą ${cardName}` : 'Napraw połączenie karty prywatnej'),
subtitle: 'Portfel',
},
+ validateAccount: {title: 'Zweryfikuj swoje konto, aby dalej korzystać z Expensify', subtitle: 'Konto', cta: 'Zatwierdź'},
},
assignedCards: 'Twoje Karty Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`,
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index dc69a1bbed4e9..e2c3fb53a3f5f 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -984,6 +984,7 @@ const translations: TranslationDeepObject = {
title: ({cardName}: {cardName?: string}) => (cardName ? `Corrigir conexão do cartão pessoal ${cardName}` : 'Corrigir conexão do cartão pessoal'),
subtitle: 'Carteira',
},
+ validateAccount: {title: 'Valide sua conta para continuar usando o Expensify', subtitle: 'Conta', cta: 'Validar'},
},
assignedCards: 'Seus Cartões Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 4c966a9baaf5d..96415f4fa273d 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -964,6 +964,7 @@ const translations: TranslationDeepObject = {
defaultSubtitle: '工作区',
subtitle: ({policyName}: {policyName: string}) => `${policyName} > 会计`,
},
+ validateAccount: {title: '验证您的账户以继续使用 Expensify', subtitle: '账户', cta: '验证'},
},
assignedCards: '你的 Expensify 卡',
assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`,
diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx
index ea9eb87903e2a..537e583d0aa08 100644
--- a/src/pages/home/TimeSensitiveSection/index.tsx
+++ b/src/pages/home/TimeSensitiveSection/index.tsx
@@ -1,15 +1,19 @@
+import {isUserValidatedSelector} from '@selectors/Account';
import {activeAdminPoliciesSelector} from '@selectors/Policy';
+import {emailSelector} from '@selectors/Session';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import WidgetContainer from '@components/WidgetContainer';
import useCardFeedErrors from '@hooks/useCardFeedErrors';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useIsAnonymousUser from '@hooks/useIsAnonymousUser';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {hasSynchronizationErrorMessage, isConnectionInProgress} from '@libs/actions/connections';
+import {isCurrentUserValidated} from '@libs/UserUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy';
@@ -24,6 +28,7 @@ import FixPersonalCardConnection from './items/FixPersonalCardConnection';
import Offer25off from './items/Offer25off';
import Offer50off from './items/Offer50off';
import ReviewCardFraud from './items/ReviewCardFraud';
+import ValidateAccount from './items/ValidateAccount';
type BrokenAccountingConnection = {
/** The policy ID associated with this connection */
@@ -57,6 +62,7 @@ function TimeSensitiveSection() {
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {login} = useCurrentUserPersonalDetails();
+ const isAnonymous = useIsAnonymousUser();
// Use custom hooks for offers and cards (Release 3)
const {shouldShow50off, shouldShow25off, shouldShowAddPaymentCard, firstDayFreeTrial, discountInfo} = useTimeSensitiveOffers();
@@ -64,8 +70,15 @@ function TimeSensitiveSection() {
// Selector for filtering admin policies (Release 4)
const adminPoliciesSelectorWrapper = useCallback((policies: OnyxCollection) => activeAdminPoliciesSelector(policies, login ?? ''), [login]);
- const [adminPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: adminPoliciesSelectorWrapper});
+ const [adminPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
+ selector: adminPoliciesSelectorWrapper,
+ });
const [connectionSyncProgress] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS);
+ const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {
+ selector: isUserValidatedSelector,
+ });
+ const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
+ const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
// Get card feed errors for company card connections (Release 4)
const cardFeedErrors = useCardFeedErrors();
@@ -132,10 +145,13 @@ function TimeSensitiveSection() {
const hasBrokenCompanyCards = brokenCompanyCardConnections.length > 0;
const hasBrokenPersonalCards = brokenPersonalCardConnections.length > 0;
const hasBrokenAccountingConnections = brokenAccountingConnections.length > 0;
+ const isCurrentLoginValidated = isCurrentUserValidated(loginList, sessionEmail ?? login);
+ const shouldShowValidateAccount = isUserValidated === false && !isAnonymous && !isCurrentLoginValidated;
// This guard must exactly match the conditions used to render each widget below.
// If a widget has additional conditions in the render (e.g. && !!discountInfo), those
// must be reflected here to avoid showing an empty "Time sensitive" section.
const hasAnyTimeSensitiveContent =
+ shouldShowValidateAccount ||
shouldShowReviewCardFraud ||
shouldShowAddPaymentCard ||
shouldShow50off ||
@@ -151,18 +167,22 @@ function TimeSensitiveSection() {
}
// Priority order:
- // 1. Potential card fraud
- // 2. Add payment card (trial ended, no payment card)
- // 3. Broken bank connections (company cards)
- // 4. Broken bank connections (personal cards)
- // 5. Broken accounting connections
- // 6. Early adoption discount (50% or 25%)
- // 7. Expensify card shipping
- // 8. Expensify card activation
+ // 1. Validate account
+ // 2. Potential card fraud
+ // 3. Add payment card (trial ended, no payment card)
+ // 4. Broken bank connections (company cards)
+ // 5. Broken bank connections (personal cards)
+ // 6. Broken accounting connections
+ // 7. Early adoption discount (50% or 25%)
+ // 8. Expensify card shipping
+ // 9. Expensify card activation
return (
- {/* Priority 1: Card fraud alerts */}
+ {/* Priority 1: Validate account */}
+ {shouldShowValidateAccount && }
+
+ {/* Priority 2: Card fraud alerts */}
{shouldShowReviewCardFraud &&
cardsWithFraud.map((card) => {
if (!card.nameValuePairs?.possibleFraud) {
@@ -176,9 +196,9 @@ function TimeSensitiveSection() {
);
})}
- {/* Priority 2: Add payment card (trial ended, no payment card) */}
+ {/* Priority 3: Add payment card (trial ended, no payment card) */}
{shouldShowAddPaymentCard && }
- {/* Priority 3: Broken company card connections */}
+ {/* Priority 4: Broken company card connections */}
{brokenCompanyCardConnections.map((connection) => {
const card = cardFeedErrors.cardsWithBrokenFeedConnection[connection.cardID];
if (!card) {
@@ -194,7 +214,7 @@ function TimeSensitiveSection() {
);
})}
- {/* Priority 4: Broken personal card connections */}
+ {/* Priority 5: Broken personal card connections */}
{brokenPersonalCardConnections.map((connection) => {
const card = cardFeedErrors.personalCardsWithBrokenConnection[connection.cardID];
if (!card) {
@@ -208,7 +228,7 @@ function TimeSensitiveSection() {
);
})}
- {/* Priority 5: Broken accounting connections */}
+ {/* Priority 6: Broken accounting connections */}
{brokenAccountingConnections.map((connection) => (
))}
- {/* Priority 6: Early adoption discount offers */}
+ {/* Priority 7: Early adoption discount offers */}
{shouldShow50off && }
{shouldShow25off && !!discountInfo && }
- {/* Priority 7: Expensify card shipping */}
+ {/* Priority 8: Expensify card shipping */}
{shouldShowAddShippingAddress &&
cardsNeedingShippingAddress.map((card) => (
))}
- {/* Priority 8: Expensify card activation */}
+ {/* Priority 9: Expensify card activation */}
{shouldShowActivateCard &&
cardsNeedingActivation.map((card) => (
Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHOD_VERIFY_ACCOUNT.getRoute())}
+ buttonProps={{success: true}}
+ />
+ );
+}
+
+export default ValidateAccount;
diff --git a/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx
new file mode 100644
index 0000000000000..a1a5407636f1d
--- /dev/null
+++ b/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx
@@ -0,0 +1,134 @@
+import {render, screen} from '@testing-library/react-native';
+import Onyx from 'react-native-onyx';
+import OnyxListItemProvider from '@src/components/OnyxListItemProvider';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import TimeSensitiveSection from '@src/pages/home/TimeSensitiveSection';
+import useTimeSensitiveOffers from '@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers';
+import waitForBatchedUpdates from '../../../../utils/waitForBatchedUpdates';
+
+jest.mock('@libs/Navigation/Navigation');
+
+jest.mock('@hooks/useLocalize', () => jest.fn(() => ({translate: jest.fn((key: string) => key)})));
+
+jest.mock('@hooks/useLazyAsset', () => ({
+ useMemoizedLazyExpensifyIcons: jest.fn(() => ({
+ EnvelopeOpenStar: () => null,
+ })),
+}));
+
+jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers', () =>
+ jest.fn(() => ({
+ shouldShow50off: false,
+ shouldShow25off: false,
+ shouldShowAddPaymentCard: false,
+ firstDayFreeTrial: undefined,
+ discountInfo: undefined,
+ })),
+);
+
+jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards', () =>
+ jest.fn(() => ({
+ shouldShowAddShippingAddress: false,
+ shouldShowActivateCard: false,
+ shouldShowReviewCardFraud: false,
+ cardsNeedingShippingAddress: [],
+ cardsNeedingActivation: [],
+ cardsWithFraud: [],
+ })),
+);
+
+jest.mock('@hooks/useCardFeedErrors', () =>
+ jest.fn(() => ({
+ cardsWithBrokenFeedConnection: {},
+ personalCardsWithBrokenConnection: {},
+ })),
+);
+
+jest.mock('@hooks/useCurrentUserPersonalDetails', () => jest.fn(() => ({login: 'test@example.com'})));
+
+jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({shouldUseNarrowLayout: false})));
+
+const renderTimeSensitiveSection = () =>
+ render(
+
+
+ ,
+ );
+
+describe('TimeSensitiveSection - ValidateAccount', () => {
+ const mockedUseTimeSensitiveOffers = jest.mocked(useTimeSensitiveOffers);
+
+ beforeAll(() => {
+ Onyx.init({keys: ONYXKEYS});
+ });
+
+ beforeEach(async () => {
+ mockedUseTimeSensitiveOffers.mockReturnValue({
+ shouldShow50off: false,
+ shouldShow25off: false,
+ shouldShowAddPaymentCard: false,
+ firstDayFreeTrial: undefined,
+ discountInfo: null,
+ });
+ await Onyx.clear();
+ await waitForBatchedUpdates();
+ });
+
+ it('shows ValidateAccount widget when account is not validated', async () => {
+ await Onyx.set(ONYXKEYS.ACCOUNT, {validated: false});
+ await Onyx.set(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT});
+ await waitForBatchedUpdates();
+
+ renderTimeSensitiveSection();
+
+ expect(screen.getByText('homePage.timeSensitiveSection.validateAccount.title')).toBeTruthy();
+ });
+
+ it('hides ValidateAccount for anonymous users while keeping time sensitive section visible', async () => {
+ mockedUseTimeSensitiveOffers.mockReturnValue({
+ shouldShow50off: false,
+ shouldShow25off: false,
+ shouldShowAddPaymentCard: true,
+ firstDayFreeTrial: undefined,
+ discountInfo: null,
+ });
+
+ await Onyx.set(ONYXKEYS.ACCOUNT, {validated: false});
+ await Onyx.set(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.ANONYMOUS});
+ await waitForBatchedUpdates();
+
+ renderTimeSensitiveSection();
+
+ expect(screen.getByText('homePage.timeSensitiveSection.title')).toBeTruthy();
+ expect(screen.getByText('homePage.timeSensitiveSection.addPaymentCard.title')).toBeTruthy();
+ expect(screen.queryByText('homePage.timeSensitiveSection.validateAccount.title')).toBeNull();
+ });
+
+ it('hides ValidateAccount when current login is already validated in login list', async () => {
+ const validatedEmail = 'test@example.com';
+
+ mockedUseTimeSensitiveOffers.mockReturnValue({
+ shouldShow50off: false,
+ shouldShow25off: false,
+ shouldShowAddPaymentCard: true,
+ firstDayFreeTrial: undefined,
+ discountInfo: null,
+ });
+
+ await Onyx.set(ONYXKEYS.ACCOUNT, {validated: false});
+ await Onyx.set(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT, email: validatedEmail});
+ await Onyx.set(ONYXKEYS.LOGIN_LIST, {
+ [validatedEmail]: {
+ validatedDate: '2026-03-18 00:00:00.000',
+ },
+ });
+ await waitForBatchedUpdates();
+
+ renderTimeSensitiveSection();
+
+ expect(screen.getByText('homePage.timeSensitiveSection.title')).toBeTruthy();
+ expect(screen.getByText('homePage.timeSensitiveSection.addPaymentCard.title')).toBeTruthy();
+ expect(screen.queryByText('homePage.timeSensitiveSection.validateAccount.title')).toBeNull();
+ });
+});