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/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: { 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: '激活'}, }, }, }; diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts new file mode 100644 index 0000000000000..909683ad61ba4 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards.ts @@ -0,0 +1,46 @@ +import {useMemo} from 'react'; +import useOnyx from '@hooks/useOnyx'; +import {isCard, 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, {canBeMissing: true}); + + const {cardsNeedingShippingAddress, cardsNeedingActivation} = useMemo<{cardsNeedingShippingAddress: Card[]; cardsNeedingActivation: Card[]}>(() => { + 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[]}>( + (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; + const shouldShowActivateCard = cardsNeedingActivation.length > 0; + + return { + shouldShowAddShippingAddress, + shouldShowActivateCard, + cardsNeedingShippingAddress, + cardsNeedingActivation, + }; +} + +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..09f45482b1bfa 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -1,17 +1,16 @@ 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 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'; @@ -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, cardsNeedingShippingAddress, cardsNeedingActivation} = 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 && + cardsNeedingShippingAddress.map((card) => ( + + ))} + {shouldShowActivateCard && + cardsNeedingActivation.map((card) => ( + + ))} ); diff --git a/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx new file mode 100644 index 0000000000000..41c1b61dbbbcf --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx @@ -0,0 +1,31 @@ +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 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)))} + /> + ); +} + +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..d2f478e27d089 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/items/AddShippingAddress.tsx @@ -0,0 +1,31 @@ +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)))} + /> + ); +} + +export default AddShippingAddress; 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};