Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
10 changes: 10 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,16 @@ const translations: TranslationDeepObject<typeof en> = {
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: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '有効化'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
},
},
};
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '激活'},
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will refactor this to a selector

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring here #81058


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;
Copy link
Contributor

@ZhenjaHorbach ZhenjaHorbach Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor issue
But insteadcard.bank === CONST.EXPENSIFY_CARD.BANK we can use isExpensifyCard utility


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;
Original file line number Diff line number Diff line change
@@ -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;
43 changes: 24 additions & 19 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 (
<WidgetContainer
icon={icons.Stopwatch}
Expand All @@ -50,7 +41,21 @@ function TimeSensitiveSection() {
>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
{shouldShow50off && <Offer50off firstDayFreeTrial={firstDayFreeTrial} />}
{shouldShow25off && <Offer25off days={discountInfo.days} />}
{shouldShow25off && !!discountInfo && <Offer25off days={discountInfo.days} />}
{shouldShowAddShippingAddress &&
cardsNeedingShippingAddress.map((card) => (
<AddShippingAddress
key={card.cardID}
card={card}
/>
))}
{shouldShowActivateCard &&
cardsNeedingActivation.map((card) => (
<ActivateCard
key={card.cardID}
card={card}
/>
))}
</View>
</WidgetContainer>
);
Expand Down
31 changes: 31 additions & 0 deletions src/pages/home/TimeSensitiveSection/items/ActivateCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<BaseWidgetItem
icon={ExpensifyCardIcon}
iconBackgroundColor={theme.widgetIconBG}
iconFill={theme.widgetIconFill}
title={translate('homePage.timeSensitiveSection.activateCard.title')}
subtitle={translate('homePage.timeSensitiveSection.activateCard.subtitle')}
ctaText={translate('homePage.timeSensitiveSection.activateCard.cta')}
onCtaPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(String(card.cardID)))}
/>
);
}

export default ActivateCard;
Original file line number Diff line number Diff line change
@@ -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 (
<BaseWidgetItem
icon={ExpensifyCardIcon}
iconBackgroundColor={theme.widgetIconBG}
iconFill={theme.widgetIconFill}
title={translate('homePage.timeSensitiveSection.addShippingAddress.title')}
subtitle={translate('homePage.timeSensitiveSection.addShippingAddress.subtitle')}
ctaText={translate('homePage.timeSensitiveSection.addShippingAddress.cta')}
onCtaPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAIN_CARD.getRoute(String(card.cardID)))}
/>
);
}

export default AddShippingAddress;
10 changes: 9 additions & 1 deletion src/selectors/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const filterOutPersonalCards = (cards: OnyxEntry<CardList>): 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>): CardList => {
return filterObject(cards ?? {}, (key, card) => isPersonalCard(card));
};
Comment on lines +31 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gonna remove this in my PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing here #81058


/**
* Selects the Expensify Card feed from the card list and returns the first one.
*/
Expand All @@ -41,4 +49,4 @@ const defaultExpensifyCardSelector = (allCards: OnyxEntry<NonPersonalAndWorkspac
*/
const cardByIdSelector = (cardID: string) => (cardList: OnyxEntry<CardList>) => cardList?.[cardID];

export {filterCardsHiddenFromSearch, filterOutPersonalCards, defaultExpensifyCardSelector, cardByIdSelector};
export {filterCardsHiddenFromSearch, filterOutPersonalCards, filterPersonalCards, defaultExpensifyCardSelector, cardByIdSelector};
Loading