Skip to content
Open
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
12 changes: 10 additions & 2 deletions src/components/BaseWidgetItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import type {ButtonProps} from './Button';
import Icon from './Icon';
import Text from './Text';

Expand All @@ -31,12 +32,18 @@ type BaseWidgetItemProps = {

/** Optional: fill color for the icon (defaults to white) */
iconFill?: string;

/** Optional: additional props to pass to the Button component (e.g., danger, success) */
buttonProps?: Partial<Pick<ButtonProps, 'success' | 'danger'>>;
};

function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, onCtaPress, iconFill}: BaseWidgetItemProps) {
function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, onCtaPress, iconFill, buttonProps}: BaseWidgetItemProps) {
const styles = useThemeStyles();
const theme = useTheme();

// Default to success style unless buttonProps specifies otherwise
const showSuccess = buttonProps?.success ?? !buttonProps?.danger;

return (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap3]}>
<View style={styles.getWidgetItemIconContainerStyle(iconBackgroundColor)}>
Expand All @@ -54,7 +61,8 @@ function BaseWidgetItem({icon, iconBackgroundColor, title, subtitle, ctaText, on
<Button
text={ctaText}
onPress={onCtaPress}
success
success={showSuccess}
danger={buttonProps?.danger}
small
style={styles.widgetItemButton}
/>
Expand Down
6 changes: 6 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8307,6 +8307,12 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
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'},
reviewCardFraud: {
title: 'Möglichen Betrug mit Ihrer Expensify Card überprüfen',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Überprüfen Sie potenziellen Betrug in Höhe von ${amount} bei ${merchant}`,
subtitle: 'Expensify Card',
cta: 'Überprüfen',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,12 @@ const translations = {
subtitle: 'Validate your card and start spending.',
cta: 'Activate',
},
reviewCardFraud: {
title: 'Review potential fraud on your Expensify Card',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Review ${amount} in potential fraud at ${merchant}`,
subtitle: 'Expensify Card',
cta: 'Review',
},
},
announcements: 'Announcements',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,12 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Valida tu tarjeta y empieza a gastar.',
cta: 'Activa',
},
reviewCardFraud: {
title: 'Revisa un posible fraude en tu tarjeta Expensify',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Revisa ${amount} en posible fraude en ${merchant}`,
subtitle: 'Tarjeta Expensify',
cta: 'Revisar',
},
},
announcements: 'Anuncios',
discoverSection: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8313,6 +8313,12 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
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'},
reviewCardFraud: {
title: 'Examiner une fraude potentielle sur votre carte Expensify',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Examiner ${amount} de fraude potentielle chez ${merchant}`,
subtitle: 'Carte Expensify',
cta: 'Vérifier',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8293,6 +8293,12 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
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'},
reviewCardFraud: {
title: 'Esamina una potenziale frode sulla tua Expensify Card',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Controlla ${amount} di potenziale frode presso ${merchant}`,
subtitle: 'Carta Expensify',
cta: 'Rivedi',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8204,6 +8204,12 @@ Expensify の使い方をお見せするための*テストレシート*がこ
offer25off: {title: '初年度が25%オフ!', subtitle: ({days}: {days: number}) => `残り ${days} ${days === 1 ? '日' : '日'}`},
addShippingAddress: {title: '配送先住所が必要です', subtitle: 'Expensify Card を受け取る住所を入力してください。', cta: '住所を追加'},
activateCard: {title: 'Expensify Card を有効化', subtitle: 'カードを認証して、すぐに支出を始めましょう。', cta: '有効化'},
reviewCardFraud: {
title: 'Expensify Card の不正利用の可能性を確認する',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `${merchant} での不正の可能性がある ${amount} を確認`,
subtitle: 'Expensify Card',
cta: 'レビュー',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8269,6 +8269,12 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`,
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'},
reviewCardFraud: {
title: 'Controleer mogelijke fraude op je Expensify Card',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Controleer ${amount} aan mogelijke fraude bij ${merchant}`,
subtitle: 'Expensify Card',
cta: 'Beoordeling',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8253,6 +8253,12 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`,
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'},
reviewCardFraud: {
title: 'Przejrzyj potencjalne oszustwo na swojej karcie Expensify',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Sprawdź ${amount} pod kątem potencjalnego oszustwa w ${merchant}`,
subtitle: 'Karta Expensify',
cta: 'Przejrzyj',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8263,6 +8263,12 @@ Aqui está um *recibo de teste* para mostrar como funciona:`,
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'},
reviewCardFraud: {
title: 'Analisar possível fraude no seu Cartão Expensify',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Revisar ${amount} em potencial fraude em ${merchant}`,
subtitle: 'Cartão Expensify',
cta: 'Revisar',
},
},
},
};
Expand Down
6 changes: 6 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8021,6 +8021,12 @@ ${reportName}
offer25off: {title: '首次年度订阅立享 25% 折扣!', subtitle: ({days}: {days: number}) => `剩余 ${days} ${days === 1 ? '天' : '天'}`},
addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify Card。', cta: '添加地址'},
activateCard: {title: '激活您的 Expensify Card', subtitle: '验证您的卡片并开始消费。', cta: '激活'},
reviewCardFraud: {
title: '审查您的 Expensify Card 潜在欺诈行为',
titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `在 ${merchant} 发现疑似欺诈金额 ${amount},请审核`,
subtitle: 'Expensify Card',
cta: '审核',
},
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
import {useMemo} from 'react';
import {timeSensitiveCardsSelector} from '@selectors/Card';
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 [timeSensitiveCards] = useOnyx(ONYXKEYS.CARD_LIST, {
canBeMissing: true,
selector: timeSensitiveCardsSelector,
});

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 cardsNeedingShippingAddress = timeSensitiveCards?.cardsNeedingShippingAddress ?? [];
const cardsNeedingActivation = timeSensitiveCards?.cardsNeedingActivation ?? [];
const cardsWithFraud = timeSensitiveCards?.cardsWithFraud ?? [];

const shouldShowAddShippingAddress = cardsNeedingShippingAddress.length > 0;
const shouldShowActivateCard = cardsNeedingActivation.length > 0;
const shouldShowReviewCardFraud = cardsWithFraud.length > 0;

return {
shouldShowAddShippingAddress,
shouldShowActivateCard,
shouldShowReviewCardFraud,
cardsNeedingShippingAddress,
cardsNeedingActivation,
cardsWithFraud,
};
}

Expand Down
13 changes: 11 additions & 2 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ActivateCard from './items/ActivateCard';
import AddShippingAddress from './items/AddShippingAddress';
import Offer25off from './items/Offer25off';
import Offer50off from './items/Offer50off';
import ReviewCardFraud from './items/ReviewCardFraud';

function TimeSensitiveSection() {
const styles = useThemeStyles();
Expand All @@ -22,9 +23,9 @@ function TimeSensitiveSection() {
const {shouldUseNarrowLayout} = useResponsiveLayout();

const {shouldShow50off, shouldShow25off, firstDayFreeTrial, discountInfo} = useTimeSensitiveOffers();
const {shouldShowAddShippingAddress, shouldShowActivateCard, cardsNeedingShippingAddress, cardsNeedingActivation} = useTimeSensitiveCards();
const {shouldShowAddShippingAddress, shouldShowActivateCard, shouldShowReviewCardFraud, cardsNeedingShippingAddress, cardsNeedingActivation, cardsWithFraud} = useTimeSensitiveCards();

const hasAnyItemToShow = shouldShow50off || shouldShow25off || shouldShowAddShippingAddress || shouldShowActivateCard;
const hasAnyItemToShow = shouldShowReviewCardFraud || shouldShow50off || shouldShow25off || shouldShowAddShippingAddress || shouldShowActivateCard;

if (!hasAnyItemToShow) {
return null;
Expand All @@ -40,6 +41,14 @@ function TimeSensitiveSection() {
titleColor={theme.danger}
>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
{/* Priority order: 1. Fraud, 2. Discounts, 3. Shipping, 4. Activation */}
{shouldShowReviewCardFraud &&
cardsWithFraud.map((card) => (
<ReviewCardFraud
key={card.cardID}
card={card}
/>
))}
{shouldShow50off && <Offer50off firstDayFreeTrial={firstDayFreeTrial} />}
{shouldShow25off && !!discountInfo && <Offer25off days={discountInfo.days} />}
{shouldShowAddShippingAddress &&
Expand Down
60 changes: 60 additions & 0 deletions src/pages/home/TimeSensitiveSection/items/ReviewCardFraud.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, {useMemo} from 'react';
import ExpensifyCardIcon from '@assets/images/expensify-card-icon.svg';
import BaseWidgetItem from '@components/BaseWidgetItem';
import useLocalize from '@hooks/useLocalize';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import colors from '@styles/theme/colors';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Card} from '@src/types/onyx';

type ReviewCardFraudProps = {
/** The card with potential fraud */
card: Card;
};

function ReviewCardFraud({card}: ReviewCardFraudProps) {
const {translate} = useLocalize();

const possibleFraud = card.message?.possibleFraud;
const fraudAlertReportID = possibleFraud?.fraudAlertReportID;
const triggerAmount = possibleFraud?.triggerAmount;
const triggerMerchant = possibleFraud?.triggerMerchant;

const handleReviewPress = () => {
if (!fraudAlertReportID) {
return;
Comment on lines +25 to +27

Choose a reason for hiding this comment

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

P2 Badge Avoid rendering a dead CTA when report ID is missing

When a card has fraud set but the optional message?.possibleFraud?.fraudAlertReportID isn’t populated (e.g., older data or partial Onyx hydration), the widget still renders while handleReviewPress bails out early. In that scenario the “Review” button is a no‑op, so users can’t reach the fraud report they’re being prompted to review. Consider gating rendering on the report ID, disabling the CTA until it exists, or linking to a fallback destination.

Useful? React with 👍 / 👎.

}

// Navigate to the report containing the fraud alert action
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(fraudAlertReportID)));
};

// Generate the title with amount and merchant if available
const title = useMemo(() => {
if (triggerAmount !== undefined && triggerMerchant) {
const formattedAmount = convertToDisplayString(triggerAmount, CONST.CURRENCY.USD);
return translate('homePage.timeSensitiveSection.reviewCardFraud.titleWithDetails', {
amount: formattedAmount,
merchant: triggerMerchant,
});
}
return translate('homePage.timeSensitiveSection.reviewCardFraud.title');
}, [triggerAmount, triggerMerchant, translate]);

return (
<BaseWidgetItem
icon={ExpensifyCardIcon}
iconBackgroundColor={colors.tangerine100}
iconFill={colors.tangerine700}
title={title}
subtitle={translate('homePage.timeSensitiveSection.reviewCardFraud.subtitle')}
ctaText={translate('homePage.timeSensitiveSection.reviewCardFraud.cta')}
onCtaPress={handleReviewPress}
buttonProps={{danger: true}}
/>
);
}

export default ReviewCardFraud;
Loading