From ac997dbb6b77f8836933863dc8f01ef77f1c300a Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:03:23 -0800 Subject: [PATCH 01/10] Add translation for cards --- src/languages/en.ts | 10 ++++++++++ src/languages/es.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 26bf02f5a72d9..02c0abe5d918a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1005,6 +1005,16 @@ const translations = { title: 'Get 25% off your first year!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'day' : 'days'} remaining`, }, + addShippingAddress: { + title: 'We need your shipping address', + subtitle: 'Provide an address to receive your Expensify Card.', + cta: 'Add address', + }, + activateCard: { + title: 'Activate your Expensify Card', + subtitle: 'Validate your card and start spending.', + cta: 'Activate', + }, }, announcements: 'Announcements', discoverSection: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1b70236bc8cf6..f3e1f3af33966 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -750,6 +750,16 @@ const translations: TranslationDeepObject = { title: '¡Obtén 25% de descuento en tu primer año!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'día' : 'días'} restantes`, }, + addShippingAddress: { + title: 'Necesitamos tu dirección de envío', + subtitle: 'Proporciona una dirección para recibir tu Tarjeta Expensify.', + cta: 'Añade dirección', + }, + activateCard: { + title: 'Activa tu Tarjeta Expensify', + subtitle: 'Valida tu tarjeta y empieza a gastar.', + cta: 'Activa', + }, }, announcements: 'Anuncios', discoverSection: { From 82b82f46538605301a4c4d1a7986228c1d2e58be Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:04:03 -0800 Subject: [PATCH 02/10] Implement card time sensitive initial --- .../hooks/useTimeSensitiveCards.ts | 41 +++++++++++++++++++ .../hooks/useTimeSensitiveOffers.ts | 30 ++++++++++++++ src/pages/home/TimeSensitiveSection/index.tsx | 31 ++++++-------- .../items/ActivateCard.tsx | 33 +++++++++++++++ .../items/AddShippingAddress.tsx | 33 +++++++++++++++ 5 files changed, 149 insertions(+), 19 deletions(-) create mode 100644 src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts create mode 100644 src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers.ts create mode 100644 src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx create mode 100644 src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts new file mode 100644 index 0000000000000..fa8809ae29577 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -0,0 +1,41 @@ +import {filterPersonalCards} from '@selectors/Card'; +import {useMemo} from 'react'; +import useOnyx from '@hooks/useOnyx'; +import {isCardPendingActivate, isCardPendingIssue} from '@libs/CardUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useTimeSensitiveCards() { + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); + + const {cardNeedingShippingAddress, cardNeedingActivation} = useMemo(() => { + const cards = Object.values(cardList ?? {}); + + // Find the first card that needs a shipping address (state: NOT_ISSUED) + const pendingIssueCard = cards.find( + (card) => isCardPendingIssue(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, + ); + + // Find the first card that needs activation (state: NOT_ACTIVATED) + const pendingActivateCard = cards.find( + (card) => isCardPendingActivate(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, + ); + + return { + cardNeedingShippingAddress: pendingIssueCard, + cardNeedingActivation: pendingActivateCard, + }; + }, [cardList]); + + const shouldShowAddShippingAddress = !!cardNeedingShippingAddress; + const shouldShowActivateCard = !!cardNeedingActivation; + + return { + shouldShowAddShippingAddress, + shouldShowActivateCard, + cardNeedingShippingAddress, + cardNeedingActivation, + }; +} + +export default useTimeSensitiveCards; diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers.ts new file mode 100644 index 0000000000000..aa88f1a6eb103 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveOffers.ts @@ -0,0 +1,30 @@ +import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing'; +import useOnyx from '@hooks/useOnyx'; +import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; +import {getEarlyDiscountInfo, shouldShowDiscountBanner} from '@libs/SubscriptionUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useTimeSensitiveOffers() { + const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, {canBeMissing: true}); + const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, {canBeMissing: true}); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); + const hasTeam2025Pricing = useHasTeam2025Pricing(); + const subscriptionPlan = useSubscriptionPlan(); + + // Use the same logic as the subscription page to determine if discount banner should be shown + const shouldShowDiscount = shouldShowDiscountBanner(hasTeam2025Pricing, subscriptionPlan, firstDayFreeTrial, lastDayFreeTrial, userBillingFundID); + const discountInfo = getEarlyDiscountInfo(firstDayFreeTrial); + + // Determine which offer to show based on discount type (they are mutually exclusive) + const shouldShow50off = shouldShowDiscount && discountInfo?.discountType === 50; + const shouldShow25off = shouldShowDiscount && discountInfo?.discountType === 25; + + return { + shouldShow50off, + shouldShow25off, + firstDayFreeTrial, + discountInfo, + }; +} + +export default useTimeSensitiveOffers; diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index 9eccd9d590be7..cd15636cd7224 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -1,19 +1,18 @@ import React from 'react'; import {View} from 'react-native'; import WidgetContainer from '@components/WidgetContainer'; -import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getEarlyDiscountInfo, shouldShowDiscountBanner} from '@libs/SubscriptionUtils'; import variables from '@styles/variables'; -import ONYXKEYS from '@src/ONYXKEYS'; +import ActivateCard from './items/ActivateCard'; +import AddShippingAddress from './items/AddShippingAddress'; import Offer25off from './items/Offer25off'; import Offer50off from './items/Offer50off'; +import useTimeSensitiveCards from './hooks/useTimeSensitiveCards'; +import useTimeSensitiveOffers from './hooks/useTimeSensitiveOffers'; function TimeSensitiveSection() { const styles = useThemeStyles(); @@ -21,24 +20,16 @@ function TimeSensitiveSection() { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [firstDayFreeTrial] = useOnyx(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, {canBeMissing: true}); - const [lastDayFreeTrial] = useOnyx(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, {canBeMissing: true}); - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); - const hasTeam2025Pricing = useHasTeam2025Pricing(); - const subscriptionPlan = useSubscriptionPlan(); - // Use the same logic as the subscription page to determine if discount banner should be shown - const shouldShowDiscount = shouldShowDiscountBanner(hasTeam2025Pricing, subscriptionPlan, firstDayFreeTrial, lastDayFreeTrial, userBillingFundID); - const discountInfo = getEarlyDiscountInfo(firstDayFreeTrial); + const {shouldShow50off, shouldShow25off, firstDayFreeTrial, discountInfo} = useTimeSensitiveOffers(); + const {shouldShowAddShippingAddress, shouldShowActivateCard, cardNeedingShippingAddress, cardNeedingActivation} = useTimeSensitiveCards(); - if (!shouldShowDiscount || !discountInfo) { + const hasAnyItemToShow = shouldShow50off || shouldShow25off || shouldShowAddShippingAddress || shouldShowActivateCard; + + if (!hasAnyItemToShow) { return null; } - // Determine which offer to show based on discount type (they are mutually exclusive) - const shouldShow50off = discountInfo.discountType === 50; - const shouldShow25off = discountInfo.discountType === 25; - return ( {shouldShow50off && } - {shouldShow25off && } + {shouldShow25off && !!discountInfo && } + {shouldShowAddShippingAddress && !!cardNeedingShippingAddress && } + {shouldShowActivateCard && cardNeedingActivation !== undefined && } ); diff --git a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx new file mode 100644 index 0000000000000..235dbe0833013 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import CreditCard from '@assets/images/creditcard.svg'; +import BaseWidgetItem from '@components/BaseWidgetItem'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {Card} from '@src/types/onyx'; + +type ActivateCardProps = { + card: Card; +}; + +function ActivateCard({card}: ActivateCardProps) { + const theme = useTheme(); + const {translate} = useLocalize(); + + return ( + Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(String(card.cardID)))} + /> + ); +} + +ActivateCard.displayName = 'ActivateCard'; + +export default ActivateCard; diff --git a/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx b/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx new file mode 100644 index 0000000000000..0003aef03d74f --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ExpensifyCardIcon from '@assets/images/expensify-card-icon.svg'; +import BaseWidgetItem from '@components/BaseWidgetItem'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import type {Card} from '@src/types/onyx'; + +type AddShippingAddressProps = { + card: Card; +}; + +function AddShippingAddress({card}: AddShippingAddressProps) { + const theme = useTheme(); + const {translate} = useLocalize(); + + return ( + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(card.cardID)))} + /> + ); +} + +AddShippingAddress.displayName = 'AddShippingAddress'; + +export default AddShippingAddress; From 92fde3d046c7a6309321f901b941beeaa622f647 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:15:05 -0800 Subject: [PATCH 03/10] Handle more that one card --- .../hooks/useTimeSensitiveCards.ts | 23 ++++++++++--------- src/pages/home/TimeSensitiveSection/index.tsx | 6 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index fa8809ae29577..4908133937003 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -4,37 +4,38 @@ import useOnyx from '@hooks/useOnyx'; import {isCardPendingActivate, isCardPendingIssue} from '@libs/CardUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Card} from '@src/types/onyx'; function useTimeSensitiveCards() { const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); - const {cardNeedingShippingAddress, cardNeedingActivation} = useMemo(() => { + const {cardsNeedingShippingAddress, cardsNeedingActivation} = useMemo<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(() => { const cards = Object.values(cardList ?? {}); - // Find the first card that needs a shipping address (state: NOT_ISSUED) - const pendingIssueCard = cards.find( + // Find all cards that need a shipping address (state: NOT_ISSUED) + const pendingIssueCards = cards.filter( (card) => isCardPendingIssue(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, ); - // Find the first card that needs activation (state: NOT_ACTIVATED) - const pendingActivateCard = cards.find( + // Find all cards that need activation (state: NOT_ACTIVATED) + const pendingActivateCards = cards.filter( (card) => isCardPendingActivate(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, ); return { - cardNeedingShippingAddress: pendingIssueCard, - cardNeedingActivation: pendingActivateCard, + cardsNeedingShippingAddress: pendingIssueCards, + cardsNeedingActivation: pendingActivateCards, }; }, [cardList]); - const shouldShowAddShippingAddress = !!cardNeedingShippingAddress; - const shouldShowActivateCard = !!cardNeedingActivation; + const shouldShowAddShippingAddress = cardsNeedingShippingAddress.length > 0; + const shouldShowActivateCard = cardsNeedingActivation.length > 0; return { shouldShowAddShippingAddress, shouldShowActivateCard, - cardNeedingShippingAddress, - cardNeedingActivation, + cardsNeedingShippingAddress, + cardsNeedingActivation, }; } diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index cd15636cd7224..da7e2a3ec33cf 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -22,7 +22,7 @@ function TimeSensitiveSection() { const {shouldUseNarrowLayout} = useResponsiveLayout(); const {shouldShow50off, shouldShow25off, firstDayFreeTrial, discountInfo} = useTimeSensitiveOffers(); - const {shouldShowAddShippingAddress, shouldShowActivateCard, cardNeedingShippingAddress, cardNeedingActivation} = useTimeSensitiveCards(); + const {shouldShowAddShippingAddress, shouldShowActivateCard, cardsNeedingShippingAddress, cardsNeedingActivation} = useTimeSensitiveCards(); const hasAnyItemToShow = shouldShow50off || shouldShow25off || shouldShowAddShippingAddress || shouldShowActivateCard; @@ -42,8 +42,8 @@ function TimeSensitiveSection() { {shouldShow50off && } {shouldShow25off && !!discountInfo && } - {shouldShowAddShippingAddress && !!cardNeedingShippingAddress && } - {shouldShowActivateCard && cardNeedingActivation !== undefined && } + {shouldShowAddShippingAddress && cardsNeedingShippingAddress.map((card) => )} + {shouldShowActivateCard && cardsNeedingActivation.map((card) => )} ); From ecbf6ac6fd6116210fff8803231eb791a2f3aaaf Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:25:01 -0800 Subject: [PATCH 04/10] Fix icon --- src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx index 235dbe0833013..2fd4dd4f76146 100644 --- a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx +++ b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import CreditCard from '@assets/images/creditcard.svg'; +import ExpensifyCardIcon from '@assets/images/expensify-card-icon.svg'; import BaseWidgetItem from '@components/BaseWidgetItem'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; @@ -17,7 +17,7 @@ function ActivateCard({card}: ActivateCardProps) { return ( Date: Thu, 29 Jan 2026 17:29:46 -0800 Subject: [PATCH 05/10] Fix prettier --- .../hooks/useTimeSensitiveCards.ts | 8 ++------ src/pages/home/TimeSensitiveSection/index.tsx | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index 4908133937003..46b109fca70f4 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -13,14 +13,10 @@ function useTimeSensitiveCards() { const cards = Object.values(cardList ?? {}); // Find all cards that need a shipping address (state: NOT_ISSUED) - const pendingIssueCards = cards.filter( - (card) => isCardPendingIssue(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, - ); + const pendingIssueCards = cards.filter((card) => isCardPendingIssue(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual); // Find all cards that need activation (state: NOT_ACTIVATED) - const pendingActivateCards = cards.filter( - (card) => isCardPendingActivate(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual, - ); + const pendingActivateCards = cards.filter((card) => isCardPendingActivate(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual); return { cardsNeedingShippingAddress: pendingIssueCards, diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index da7e2a3ec33cf..09f45482b1bfa 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -7,12 +7,12 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import useTimeSensitiveCards from './hooks/useTimeSensitiveCards'; +import useTimeSensitiveOffers from './hooks/useTimeSensitiveOffers'; import ActivateCard from './items/ActivateCard'; import AddShippingAddress from './items/AddShippingAddress'; import Offer25off from './items/Offer25off'; import Offer50off from './items/Offer50off'; -import useTimeSensitiveCards from './hooks/useTimeSensitiveCards'; -import useTimeSensitiveOffers from './hooks/useTimeSensitiveOffers'; function TimeSensitiveSection() { const styles = useThemeStyles(); @@ -42,8 +42,20 @@ function TimeSensitiveSection() { {shouldShow50off && } {shouldShow25off && !!discountInfo && } - {shouldShowAddShippingAddress && cardsNeedingShippingAddress.map((card) => )} - {shouldShowActivateCard && cardsNeedingActivation.map((card) => )} + {shouldShowAddShippingAddress && + cardsNeedingShippingAddress.map((card) => ( + + ))} + {shouldShowActivateCard && + cardsNeedingActivation.map((card) => ( + + ))} ); From 5361d7e41b311f746f5ef78bd0bb53f9cf227748 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:43:57 -0800 Subject: [PATCH 06/10] Add auto translations --- src/languages/de.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ 8 files changed, 16 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 62560e95799d4..ffdfd6626ec5e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8289,6 +8289,8 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, cta: 'Antrag', offer50off: {title: 'Erhalte 50 % Rabatt auf dein erstes Jahr!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} verbleibend`}, offer25off: {title: 'Erhalten Sie 25 % Rabatt auf Ihr erstes Jahr!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'Tag' : 'Tage'} verbleiben`}, + addShippingAddress: {title: 'Wir benötigen Ihre Versandadresse', subtitle: 'Gib eine Adresse an, um deine Expensify Card zu erhalten.', cta: 'Adresse hinzufügen'}, + activateCard: {title: 'Aktiviere deine Expensify Card', subtitle: 'Bestätige deine Karte und beginne mit dem Ausgeben.', cta: 'Aktivieren'}, }, }, }; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index f449e62639134..5072aab4a98ba 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8295,6 +8295,8 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`, cta: 'Demande', offer50off: {title: 'Obtenez 50 % de réduction sur votre première année !', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} restant`}, offer25off: {title: 'Obtenez 25 % de réduction sur votre première année !', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'jour' : 'jours'} restants`}, + addShippingAddress: {title: 'Nous avons besoin de votre adresse de livraison', subtitle: 'Indiquez une adresse pour recevoir votre carte Expensify.', cta: 'Ajouter une adresse'}, + activateCard: {title: 'Activer votre carte Expensify', subtitle: 'Validez votre carte et commencez à dépenser.', cta: 'Activer'}, }, }, }; diff --git a/src/languages/it.ts b/src/languages/it.ts index 49d78de81efbd..6bb77f20cab32 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8275,6 +8275,8 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`, cta: 'Richiesta', offer50off: {title: 'Ottieni il 50% di sconto sul tuo primo anno!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} rimanenti`}, offer25off: {title: 'Ottieni il 25% di sconto sul tuo primo anno!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'giorno' : 'giorni'} rimanenti`}, + addShippingAddress: {title: 'Abbiamo bisogno del tuo indirizzo di spedizione', subtitle: 'Fornisci un indirizzo per ricevere la tua Expensify Card.', cta: 'Aggiungi indirizzo'}, + activateCard: {title: 'Attiva la tua Expensify Card', subtitle: 'Convalida la tua carta e inizia a spendere.', cta: 'Attiva'}, }, }, }; diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4fdad6e86b5f6..85d581b01398f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8187,6 +8187,8 @@ Expensify の使い方をお見せするための*テストレシート*がこ cta: '申請', offer50off: {title: '初年度が50%オフ!', subtitle: ({formattedTime}: {formattedTime: string}) => `残り${formattedTime}`}, offer25off: {title: '初年度が25%オフ!', subtitle: ({days}: {days: number}) => `残り ${days} ${days === 1 ? '日' : '日'}`}, + addShippingAddress: {title: '配送先住所が必要です', subtitle: 'Expensify Card を受け取る住所を入力してください。', cta: '住所を追加'}, + activateCard: {title: 'Expensify Card を有効化', subtitle: 'カードを認証して、すぐに支出を始めましょう。', cta: '有効化'}, }, }, }; diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5399ad49a4c69..93f510ab72419 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8251,6 +8251,8 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`, cta: 'Declaratie', offer50off: {title: 'Krijg 50% korting op je eerste jaar!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} resterend`}, offer25off: {title: 'Krijg 25% korting op je eerste jaar!', subtitle: ({days}: {days: number}) => `Nog ${days} ${days === 1 ? 'dag' : 'dagen'} resterend`}, + addShippingAddress: {title: 'We hebben je verzendadres nodig', subtitle: 'Voer een adres in om je Expensify Card te ontvangen.', cta: 'Adres toevoegen'}, + activateCard: {title: 'Activeer je Expensify Card', subtitle: 'Valideer je kaart en begin met uitgeven.', cta: 'Activeren'}, }, }, }; diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b1a644a511113..2610d5b0a6b05 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8236,6 +8236,8 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`, cta: 'Roszczenie', offer50off: {title: 'Uzyskaj 50% zniżki na pierwszy rok!', subtitle: ({formattedTime}: {formattedTime: string}) => `Pozostało: ${formattedTime}`}, offer25off: {title: 'Uzyskaj 25% zniżki na pierwszy rok!', subtitle: ({days}: {days: number}) => `Pozostało ${days} ${days === 1 ? 'dzień' : 'dni'}`}, + addShippingAddress: {title: 'Potrzebujemy Twojego adresu do wysyłki', subtitle: 'Podaj adres, na który mamy wysłać Twoją kartę Expensify.', cta: 'Dodaj adres'}, + activateCard: {title: 'Aktywuj swoją kartę Expensify', subtitle: 'Zweryfikuj swoją kartę i zacznij wydawać.', cta: 'Aktywuj'}, }, }, }; diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7afaa133225a7..3ea38ef921813 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8245,6 +8245,8 @@ Aqui está um *recibo de teste* para mostrar como funciona:`, cta: 'Solicitação', offer50off: {title: 'Ganhe 50% de desconto no seu primeiro ano!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} restante`}, offer25off: {title: 'Ganhe 25% de desconto no seu primeiro ano!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'dia' : 'dias'} restantes`}, + addShippingAddress: {title: 'Precisamos do seu endereço de entrega', subtitle: 'Forneça um endereço para receber seu Expensify Card.', cta: 'Adicionar endereço'}, + activateCard: {title: 'Ative seu Cartão Expensify', subtitle: 'Valide seu cartão e comece a gastar.', cta: 'Ativar'}, }, }, }; diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index a1411321c4714..8ae49bdf2b894 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8005,6 +8005,8 @@ ${reportName} cta: '报销申请', offer50off: {title: '首年立享五折优惠!', subtitle: ({formattedTime}: {formattedTime: string}) => `剩余 ${formattedTime}`}, offer25off: {title: '首次年度订阅立享 25% 折扣!', subtitle: ({days}: {days: number}) => `剩余 ${days} ${days === 1 ? '天' : '天'}`}, + addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify Card。', cta: '添加地址'}, + activateCard: {title: '激活您的 Expensify Card', subtitle: '验证您的卡片并开始消费。', cta: '激活'}, }, }, }; From 5d48ae8e6b5d6c4576a4cc3dc27114b68ed53256 Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 17:52:51 -0800 Subject: [PATCH 07/10] Fix card selector --- src/selectors/Card.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/selectors/Card.ts b/src/selectors/Card.ts index ea83574d4a5d5..f8c1cba43866b 100644 --- a/src/selectors/Card.ts +++ b/src/selectors/Card.ts @@ -28,6 +28,14 @@ const filterOutPersonalCards = (cards: OnyxEntry): CardList => { return filterObject(cards ?? {}, (key, card) => !isPersonalCard(card)); }; +/** + * Filter to keep only personal cards from the card list. + * Personal cards have fundID === '0' or no fundID. + */ +const filterPersonalCards = (cards: OnyxEntry): CardList => { + return filterObject(cards ?? {}, (key, card) => isPersonalCard(card)); +}; + /** * Selects the Expensify Card feed from the card list and returns the first one. */ @@ -41,4 +49,4 @@ const defaultExpensifyCardSelector = (allCards: OnyxEntry (cardList: OnyxEntry) => cardList?.[cardID]; -export {filterCardsHiddenFromSearch, filterOutPersonalCards, defaultExpensifyCardSelector, cardByIdSelector}; +export {filterCardsHiddenFromSearch, filterOutPersonalCards, filterPersonalCards, defaultExpensifyCardSelector, cardByIdSelector}; From c3bad00af086739c4a54969230986d23c95bbf6a Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Thu, 29 Jan 2026 18:25:24 -0800 Subject: [PATCH 08/10] Remove selector --- .../home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index 46b109fca70f4..831a57b9a8a9c 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -1,4 +1,3 @@ -import {filterPersonalCards} from '@selectors/Card'; import {useMemo} from 'react'; import useOnyx from '@hooks/useOnyx'; import {isCardPendingActivate, isCardPendingIssue} from '@libs/CardUtils'; @@ -7,7 +6,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Card} from '@src/types/onyx'; function useTimeSensitiveCards() { - const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const {cardsNeedingShippingAddress, cardsNeedingActivation} = useMemo<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(() => { const cards = Object.values(cardList ?? {}); From f0493a6adfcaae1bfe61cdd874c59881f34cc9bf Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 30 Jan 2026 09:04:01 -0800 Subject: [PATCH 09/10] Apply chanage requests --- .../hooks/useTimeSensitiveCards.ts | 31 ++++++++++++------- .../items/ActivateCard.tsx | 2 -- .../items/AddShippingAddress.tsx | 2 -- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index 831a57b9a8a9c..14c54aa2b0f56 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -10,17 +10,26 @@ function useTimeSensitiveCards() { const {cardsNeedingShippingAddress, cardsNeedingActivation} = useMemo<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(() => { const cards = Object.values(cardList ?? {}); - - // Find all cards that need a shipping address (state: NOT_ISSUED) - const pendingIssueCards = cards.filter((card) => isCardPendingIssue(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual); - - // Find all cards that need activation (state: NOT_ACTIVATED) - const pendingActivateCards = cards.filter((card) => isCardPendingActivate(card) && card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual); - - return { - cardsNeedingShippingAddress: pendingIssueCards, - cardsNeedingActivation: pendingActivateCards, - }; + const isPhysicalExpensifyCard = (card: Card) => card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual; + + return cards.reduce<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>( + (acc, card) => { + if (!isPhysicalExpensifyCard(card)) { + return acc; + } + + if (isCardPendingIssue(card)) { + acc.cardsNeedingShippingAddress.push(card); + } + + if (isCardPendingActivate(card)) { + acc.cardsNeedingActivation.push(card); + } + + return acc; + }, + {cardsNeedingShippingAddress: [], cardsNeedingActivation: []}, + ); }, [cardList]); const shouldShowAddShippingAddress = cardsNeedingShippingAddress.length > 0; diff --git a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx index 2fd4dd4f76146..41c1b61dbbbcf 100644 --- a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx +++ b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx @@ -28,6 +28,4 @@ function ActivateCard({card}: ActivateCardProps) { ); } -ActivateCard.displayName = 'ActivateCard'; - export default ActivateCard; diff --git a/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx b/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx index 0003aef03d74f..d2f478e27d089 100644 --- a/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx +++ b/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx @@ -28,6 +28,4 @@ function AddShippingAddress({card}: AddShippingAddressProps) { ); } -AddShippingAddress.displayName = 'AddShippingAddress'; - export default AddShippingAddress; From 43706bb017fc6c491270d1f45bcb5091c4eee61d Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Fri, 30 Jan 2026 11:59:19 -0800 Subject: [PATCH 10/10] Fix cards filtering --- .../home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts index 14c54aa2b0f56..909683ad61ba4 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -1,6 +1,6 @@ import {useMemo} from 'react'; import useOnyx from '@hooks/useOnyx'; -import {isCardPendingActivate, isCardPendingIssue} from '@libs/CardUtils'; +import {isCard, isCardPendingActivate, isCardPendingIssue} from '@libs/CardUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card} from '@src/types/onyx'; @@ -9,7 +9,7 @@ function useTimeSensitiveCards() { const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const {cardsNeedingShippingAddress, cardsNeedingActivation} = useMemo<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(() => { - const cards = Object.values(cardList ?? {}); + const cards = Object.values(cardList ?? {}).filter(isCard); const isPhysicalExpensifyCard = (card: Card) => card.bank === CONST.EXPENSIFY_CARD.BANK && !card.nameValuePairs?.isVirtual; return cards.reduce<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(