diff --git a/assets/images/envelope-open-star.svg b/assets/images/envelope-open-star.svg new file mode 100644 index 000000000000..74652c126f5f --- /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 ac51f4f4ceec..d595fd80213d 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -48,6 +48,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'; @@ -213,6 +214,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 ffcc6da25634..e8e51555cdf6 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -79,6 +79,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'; @@ -315,6 +316,7 @@ const Expensicons = { DragHandles, EReceiptIcon, Emoji, + EnvelopeOpenStar, EmptyStateRoutePending, ExpenseCopy, Exclamation, diff --git a/src/languages/de.ts b/src/languages/de.ts index f108b80818fc..e271ac1c028e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1018,6 +1018,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 bed40da4adbf..eb4318e08d6c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1045,6 +1045,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 f48f74f2ebf7..b09c72b92d87 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -883,6 +883,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 bdc8657977c2..6b7e87967ab4 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1021,6 +1021,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 770e1d4977cd..89983ae66fe2 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1017,6 +1017,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 a4dc0c58f836..c8ab9e2a9ddd 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1009,6 +1009,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 e2712b3f7ab1..2f2c372b3a4d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1016,6 +1016,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 b55c4643497d..bda8efc92c67 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1017,6 +1017,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 251c83590f80..68b3d94eb874 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1015,6 +1015,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 f4e29f61bfda..8cedd6d6dfe8 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -995,6 +995,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 ea9eb87903e2..32886ed15071 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -1,3 +1,4 @@ +import {isUserValidatedSelector} from '@selectors/Account'; import {activeAdminPoliciesSelector} from '@selectors/Policy'; import React, {useCallback} from 'react'; import {View} from 'react-native'; @@ -24,6 +25,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 */ @@ -64,8 +66,13 @@ 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, + }); // Get card feed errors for company card connections (Release 4) const cardFeedErrors = useCardFeedErrors(); @@ -132,10 +139,12 @@ function TimeSensitiveSection() { const hasBrokenCompanyCards = brokenCompanyCardConnections.length > 0; const hasBrokenPersonalCards = brokenPersonalCardConnections.length > 0; const hasBrokenAccountingConnections = brokenAccountingConnections.length > 0; + const shouldShowValidateAccount = isUserValidated === false; // 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 +160,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 +189,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 +207,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 +221,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 000000000000..279dfc0e76fd --- /dev/null +++ b/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx @@ -0,0 +1,84 @@ +import {render, screen} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import OnyxListItemProvider from '@src/components/OnyxListItemProvider'; +import ONYXKEYS from '@src/ONYXKEYS'; +import TimeSensitiveSection from '@src/pages/home/TimeSensitiveSection'; +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', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + it('shows ValidateAccount widget when account is not validated', async () => { + await Onyx.set(ONYXKEYS.ACCOUNT, {validated: false}); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.getByText('homePage.timeSensitiveSection.validateAccount.title')).toBeTruthy(); + }); + + it('hides ValidateAccount widget when account is validated', async () => { + await Onyx.set(ONYXKEYS.ACCOUNT, {validated: true}); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + + expect(screen.queryByText('homePage.timeSensitiveSection.validateAccount.title')).toBeNull(); + }); +});