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
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The **Time-sensitive alerts** section appears only when there is something that
These alerts appear when:
- A workflow is blocked, such as a broken bank or accounting connection due to expired or invalid credentials
- A bank account is locked, such as a business or personal bank account that has been locked due to a failed debit or bank-side restriction
- A reimbursement is waiting for the payee to add a personal deposit account
- There is potential risk, such as suspected Expensify Card fraud on an active card
- An action must be taken within a short window, such as a limited-time offer or early adoption discount

Expand Down
6 changes: 6 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3777,6 +3777,12 @@ const CONST = {
BANK_ACCOUNT: 'bankAccountID',
},

/** The payment method a payee still has to set up before a queued payment can settle */
MISSING_PAYMENT_METHODS: {
BANK_ACCOUNT: 'bankAccount',
WALLET: 'wallet',
},

IOU: {
MAX_RECENT_REPORTS_TO_SHOW: 5,
MAX_RECENT_ATTENDEES: 40,
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ const DYNAMIC_ROUTES = {
path: 'add-bank-account/verify-account',
entryScreens: [
SCREENS.SETTINGS.WALLET.ROOT,
SCREENS.HOME,
SCREENS.RIGHT_MODAL.SEARCH_REPORT,
SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
SCREENS.REPORT,
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Zeitkritisch',
addShippingAddress: {title: 'Wir benötigen deine Versandadresse', subtitle: 'Geben Sie eine Adresse an, um Ihre Expensify Karte zu erhalten.', cta: 'Adresse hinzufügen'},
addPaymentCard: {title: 'Fügen Sie eine Zahlungskarte hinzu, um Expensify weiter zu nutzen', subtitle: 'Konto > Abonnement', cta: 'Hinzufügen'},
addBankAccount: {title: 'Fügen Sie ein Bankkonto hinzu, um eine Erstattung zu erhalten'},
activateCard: {title: 'Aktivieren Sie Ihre Expensify Karte', subtitle: 'Validieren Sie Ihre Karte und beginnen Sie mit dem Ausgeben.', cta: 'Aktivieren'},
reviewCardFraud: {
title: 'Möglichen Betrug mit Ihrer Expensify Karte überprüfen',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,10 @@ const translations = {
subtitle: 'Account > Subscription',
cta: 'Add',
},
addBankAccount: {
// @context Home reminder shown to a payment recipient when a reimbursement is waiting for them to add a personal deposit account.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is this @context

@sharabai sharabai Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure why @context is used to provide context for the strings 🤔, but that's the convention I've seen in this file. There are lots of examples that do the same thing.

title: 'Add a bank account to get reimbursed',
},
activateCard: {
title: 'Activate your Expensify Card',
subtitle: 'Validate your card and start spending.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ const translations: TranslationDeepObject<typeof en> = {
subtitle: 'Cuenta > Suscripción',
cta: 'Añadir',
},
addBankAccount: {title: 'Añade una cuenta bancaria para recibir el reembolso'},
activateCard: {
title: 'Activa tu Tarjeta Expensify',
subtitle: 'Valida tu tarjeta y empieza a gastar.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Urgent',
addShippingAddress: {title: 'Nous avons besoin de votre adresse de livraison', subtitle: 'Indiquez une adresse pour recevoir votre Carte Expensify.', cta: 'Ajouter une adresse'},
addPaymentCard: {title: 'Ajoutez une carte de paiement pour continuer à utiliser Expensify', subtitle: 'Compte > Abonnement', cta: 'Ajouter'},
addBankAccount: {title: 'Ajoutez un compte bancaire pour être remboursé'},
activateCard: {title: 'Activer votre Carte Expensify', subtitle: 'Validez votre carte et commencez à dépenser.', cta: 'Activer'},
reviewCardFraud: {
title: 'Examiner une éventuelle fraude sur votre Carte Expensify',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Sensibile al tempo',
addShippingAddress: {title: 'Ci serve il tuo indirizzo di spedizione', subtitle: 'Fornisci un indirizzo per ricevere la tua Carta Expensify.', cta: 'Aggiungi indirizzo'},
addPaymentCard: {title: 'Aggiungi una carta di pagamento per continuare a usare Expensify', subtitle: 'Account > Abbonamento', cta: 'Aggiungi'},
addBankAccount: {title: 'Aggiungi un conto bancario per ricevere il rimborso'},
activateCard: {title: 'Attiva la tua Carta Expensify', subtitle: 'Convalida la tua carta e inizia a spendere.', cta: 'Attiva'},
reviewCardFraud: {
title: 'Esamina una possibile frode sulla tua Carta Expensify',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: '時間に敏感',
addShippingAddress: {title: '配送先住所が必要です', subtitle: 'Expensify カードを受け取る住所を入力してください。', cta: '住所を追加'},
addPaymentCard: {title: 'Expensify を引き続きご利用いただくには、支払いカードを追加してください', subtitle: 'アカウント > サブスクリプション', cta: '追加'},
addBankAccount: {title: '銀行口座を追加して払い戻しを受け取りましょう'},
activateCard: {title: 'Expensify カードを有効化する', subtitle: 'カードを認証して支出を始めましょう。', cta: '有効化'},
reviewCardFraud: {
title: 'Expensify カードの不正利用の可能性を確認する',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Tijdgevoelig',
addShippingAddress: {title: 'We hebben je verzendadres nodig', subtitle: 'Geef een adres op om je Expensify Kaart te ontvangen.', cta: 'Adres toevoegen'},
addPaymentCard: {title: 'Voeg een betaalkaart toe om Expensify te blijven gebruiken', subtitle: 'Account > Abonnement', cta: 'Toevoegen'},
addBankAccount: {title: 'Voeg een bankrekening toe om je terugbetaling te ontvangen'},
activateCard: {title: 'Activeer je Expensify Kaart', subtitle: 'Valideer je kaart en begin met uitgeven.', cta: 'Activeren'},
reviewCardFraud: {
title: 'Controleer mogelijk misbruik van je Expensify Kaart',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Wymaga szybkiej reakcji',
addShippingAddress: {title: 'Potrzebujemy Twojego adresu wysyłki', subtitle: 'Podaj adres, na który mamy wysłać twoją Kartę Expensify.', cta: 'Dodaj adres'},
addPaymentCard: {title: 'Dodaj kartę płatniczą, żeby dalej korzystać z Expensify', subtitle: 'Konto > Subskrypcja', cta: 'Dodaj'},
addBankAccount: {title: 'Dodaj konto bankowe, aby otrzymać zwrot'},
activateCard: {title: 'Aktywuj swoją Kartę Expensify', subtitle: 'Zatwierdź swoją kartę i zacznij wydawać.', cta: 'Aktywuj'},
reviewCardFraud: {
title: 'Sprawdź potencjalne oszustwo na swojej Karcie Expensify',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'Urgente',
addShippingAddress: {title: 'Precisamos do seu endereço de entrega', subtitle: 'Informe um endereço para receber seu Cartão Expensify.', cta: 'Adicionar endereço'},
addPaymentCard: {title: 'Adicione um cartão de pagamento para continuar usando o Expensify', subtitle: 'Conta > Assinatura', cta: 'Adicionar'},
addBankAccount: {title: 'Adicione uma conta bancária para ser reembolsado'},
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',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,7 @@ const translations: TranslationDeepObject<typeof en> = {
title: '时间敏感',
addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify 卡。', cta: '添加地址'},
addPaymentCard: {title: '添加支付卡以继续使用 Expensify', subtitle: '账户 > 订阅', cta: '添加'},
addBankAccount: {title: '添加银行账户以接收报销'},
activateCard: {title: '激活你的 Expensify 卡', subtitle: '验证您的银行卡并开始消费。', cta: '启用'},
reviewCardFraud: {
title: '审查您 Expensify 卡上的潜在欺诈交易',
Expand Down
32 changes: 22 additions & 10 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ import {
isMovedAction,
isOlderReportAction,
isPendingRemove,
isReimbursementQueuedAction,
isReopenedAction,
isReportActionVisible,
isReportPreviewAction,
Expand Down Expand Up @@ -933,7 +932,7 @@ type Ancestor = {
shouldDisplayNewMarker: boolean;
};

type MissingPaymentMethod = 'bankAccount' | 'wallet';
type MissingPaymentMethod = ValueOf<typeof CONST.MISSING_PAYMENT_METHODS>;

type OutstandingChildRequest = {
hasOutstandingChildRequest?: boolean;
Expand Down Expand Up @@ -11449,22 +11448,34 @@ function isAllowedToApproveExpenseReport(report: OnyxEntry<Report>, approverAcco
/**
* What missing payment method does this report action indicate, if any?
*/
function getMissingPaymentMethodForQueuedPayment(
Comment thread
sharabai marked this conversation as resolved.
userWalletTierName: string | undefined,
reportAction: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED>,
bankAccountList: OnyxEntry<BankAccountList>,
): MissingPaymentMethod | undefined {
const paymentType = getOriginalMessage(reportAction)?.paymentType;
if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? CONST.MISSING_PAYMENT_METHODS.WALLET : undefined;
}

return !hasCreditBankAccount(bankAccountList) ? CONST.MISSING_PAYMENT_METHODS.BANK_ACCOUNT : undefined;
}

/**
* What missing payment method does this action indicate for the current report submitter, if any?
*/
function getIndicatedMissingPaymentMethod(
userWalletTierName: string | undefined,
reportId: string | undefined,
reportAction: ReportAction,
reportAction: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED>,
bankAccountList: OnyxEntry<BankAccountList>,
): MissingPaymentMethod | undefined {
const isSubmitterOfUnsettledReport = reportId && isCurrentUserSubmitter(getReport(reportId, deprecatedAllReports)) && !isSettled(reportId);
if (!reportId || !isSubmitterOfUnsettledReport || !isReimbursementQueuedAction(reportAction)) {
const isSubmitterOfUnsettledReport = !!reportId && isCurrentUserSubmitter(getReport(reportId, deprecatedAllReports)) && !isSettled(reportId);
if (!isSubmitterOfUnsettledReport) {
return undefined;
}
const paymentType = getOriginalMessage(reportAction)?.paymentType;
if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) {
return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined;
}

return !hasCreditBankAccount(bankAccountList) ? 'bankAccount' : undefined;
return getMissingPaymentMethodForQueuedPayment(userWalletTierName, reportAction, bankAccountList);
}

/**
Expand Down Expand Up @@ -13937,6 +13948,7 @@ export {
sortIconsByName,
getIconsForParticipants,
getIndicatedMissingPaymentMethod,
getMissingPaymentMethodForQueuedPayment,
getLastVisibleMessage,
getMoneyRequestSpendBreakdown,
getNonHeldAndFullAmount,
Expand Down
6 changes: 5 additions & 1 deletion src/pages/AddPersonalBankAccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';

import getActiveTabName from '@libs/Navigation/helpers/getActiveTabName';
import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';

Expand All @@ -18,6 +19,7 @@ import {continueSetup} from '@userActions/PaymentMethods';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';

import React, {useContext, useEffect} from 'react';

Expand All @@ -29,13 +31,15 @@ function AddPersonalBankAccountPage() {
const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT);
const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false;
const topmostFullScreenRoute = navigationRef.current?.getRootState()?.routes.findLast((route) => isFullScreenName(route.name));
const activeTab = getActiveTabName(topmostFullScreenRoute);
const kycWallRef = useContext(KYCWallContext);

const goBack = () => {
switch (topmostFullScreenRoute?.name) {
switch (activeTab) {
case NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR:
Navigation.goBack(ROUTES.SETTINGS_WALLET);
break;
case SCREENS.HOME:
Comment thread
sharabai marked this conversation as resolved.
case NAVIGATORS.REPORTS_SPLIT_NAVIGATOR:
Navigation.closeRHPFlow();
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Identifies queued payments that require the current user to add a personal bank account.
*/
import useOnyx from '@hooks/useOnyx';

import hasCreditBankAccount from '@libs/actions/ReimbursementAccount/hasCreditBankAccount';
import {isNewerReportAction, isReimbursementQueuedAction} from '@libs/ReportActionsUtils';
import {getMissingPaymentMethodForQueuedPayment} from '@libs/ReportUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, ReportAction, ReportActions} from '@src/types/onyx';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';

import type {OnyxCollection} from 'react-native-onyx';

import {accountIDSelector} from '@selectors/Session';
import {tierNameSelector} from '@selectors/UserWallet';

type ReimbursementQueuedAction = ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED>;

function getLatestReimbursementQueuedAction(reportID: string, allReportActions: OnyxCollection<ReportActions>): ReimbursementQueuedAction | undefined {
let latestAction: ReimbursementQueuedAction | undefined;
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
Comment thread
sharabai marked this conversation as resolved.

for (const action of Object.values(reportActions ?? {})) {
if (isReimbursementQueuedAction(action) && (!latestAction || isNewerReportAction(action, latestAction))) {
latestAction = action;
}
}

return latestAction;
}

function useTimeSensitiveAddBankAccount() {
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
const waitingReportIDsSelector = (allReports: OnyxCollection<Report>): string[] =>
Comment thread
sharabai marked this conversation as resolved.
Object.values(allReports ?? {})
.filter((report): report is Report => !!report?.reportID && report.isWaitingOnBankAccount === true && report.ownerAccountID === accountID)
Comment on lines +38 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we add !isSettled(report) to this filter @sharabai

@sharabai sharabai Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@grgia Great question! As far as I can tell, we don't need it. REIMBURSEMENT_QUEUED remains in the action history even after the report transitions to REIMBURSED. That's why getIndicatedMissingPaymentMethod() uses !isSettled() - to avoid acting on that historical action once the report has transitioned to REIMBURSED.

Our selector already requires isWaitingOnBankAccount === true, so adding !isSettled() would only guard against an invalid REIMBURSED + isWaitingOnBankAccount state. I tested the full flow, and the backend cleared the flag when the report transitioned to REIMBURSED, and that persisted after a refresh.

.map((report) => report.reportID);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we use reduce to decrease the number of operations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion, thanks! In this case, though, .map() only runs on the filtered result, which is at most one report in practice, so there's nothing meaningful to save here. I'd rather keep filter().map() for readability.

const [waitingReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: waitingReportIDsSelector});
const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector});
const canShowAddBankAccount = accountID !== undefined && !isLoadingOnyxValue(bankAccountListMetadata) && !hasCreditBankAccount(bankAccountList);

const shouldShowAddBankAccountSelector = (allReportActions: OnyxCollection<ReportActions>): boolean => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's move this to the selectors folder for consistency
And let's add tests

@sharabai sharabai Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ZhenjaHorbach Hmm, I had a look before replying, and I'm not sure we have a consistent convention here:

  • src/selectors/ has 141 exports across 26 files.
  • Selectors defined next to the code that uses them are at least as common: roughly 140 named ones across ~115 files, plus another ~70 written inline at the useOnyx call site.

So moving selectors into src/selectors/ doesn't really seem to be the dominant pattern. It also doesn't seem to provide much benefit: 59 of those 141 exports are referenced from exactly one file in src/.

With that in mind, I'd rather keep this one next to the hook, especially since it isn't just projecting a single Onyx value. It would need canShowAddBankAccount, waitingReportIDs, userWalletTierName, and bankAccountList passed in, three of which are themselves the results of useOnyx calls.

When you mentioned consistency, did you have something more specific in mind? Happy to move it if I'm missing the pattern you're pointing out.

if (!canShowAddBankAccount) {
return false;
}

return (waitingReportIDs ?? []).some((reportID) => {
const queuedAction = getLatestReimbursementQueuedAction(reportID, allReportActions);
Comment thread
sharabai marked this conversation as resolved.
return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === CONST.MISSING_PAYMENT_METHODS.BANK_ACCOUNT;
});
};
const [shouldShowAddBankAccount = false] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: shouldShowAddBankAccountSelector});

return {shouldShowAddBankAccount};
}

export default useTimeSensitiveAddBankAccount;
Loading
Loading