Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64c9cf6
Add unit tests for UnlockBankAccount time-sensitive widget
adamgrzybowski Apr 2, 2026
810078e
Add translations
adamgrzybowski Apr 3, 2026
a0629f0
Add UnlockBankAccount widget to TimeSensitiveSection
adamgrzybowski Apr 3, 2026
cdf5c6e
Add BankLock icon asset and export
adamgrzybowski Apr 21, 2026
cca8e51
Update UnlockBankAccount concierge chat handling
adamgrzybowski Apr 21, 2026
55da97e
Extract locked bank account detection into a dedicated time-sensitive…
adamgrzybowski Apr 21, 2026
c7382b2
Add Spanish translations for locked bank account widget
adamgrzybowski Apr 21, 2026
0cfaf89
Refactor locked bank account hook to source Onyx data
adamgrzybowski Apr 21, 2026
c06d290
Update bank-lock SVG compression
adamgrzybowski Apr 21, 2026
0b1e2eb
Fix es translation
adamgrzybowski Apr 21, 2026
799930e
Fix locked bank account widget gating for reimbursers
adamgrzybowski Apr 22, 2026
5cf022e
Fix null-safe locked bank account iteration
adamgrzybowski Apr 22, 2026
eab18e2
Fix personal locked bank account type filtering
adamgrzybowski Apr 22, 2026
292f1fb
Fix duplicate keys for locked bank account widgets
adamgrzybowski Apr 22, 2026
7940279
Add unlock bank account translations for additional locales
adamgrzybowski Apr 22, 2026
ead2ab8
Fix locked bank account iteration when list is undefined
adamgrzybowski Apr 22, 2026
63d4336
Add workspace subtitles for locked bank account locales
adamgrzybowski Apr 22, 2026
9f797cb
Merge branch 'main' into @adamgrzybowski/time-sensitive-locked-VBA
adamgrzybowski Apr 22, 2026
928ff87
Fix namespace imports in UnlockBankAccountTest to use named imports
adamgrzybowski Apr 24, 2026
7f88888
Remove .cursor
adamgrzybowski Apr 24, 2026
92090ba
Fix duplicate key and lint issues in locked bank account tests
adamgrzybowski Apr 27, 2026
9306abd
Fix workspaceLockedBankAccountIDs ordering to allow personal widget f…
adamgrzybowski Apr 27, 2026
7f8c40f
Merge branch 'main' into @adamgrzybowski/time-sensitive-locked-VBA
adamgrzybowski Apr 30, 2026
30ee9be
Merge branch 'main' into @adamgrzybowski/time-sensitive-locked-VBA
adamgrzybowski May 4, 2026
83ca9a5
Pass delegateAccountID to pressLockedBankAccount in UnlockBankAccount
adamgrzybowski May 4, 2026
1dd9cb4
Fix UnlockBankAccount test assertion to include delegateAccountID arg…
adamgrzybowski May 4, 2026
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
1 change: 1 addition & 0 deletions assets/images/bank-lock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/Icon/chunks/expensify-icons.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FallbackWorkspaceAvatar from '@assets/images/avatars/fallback-workspace-a
import NotificationsAvatar from '@assets/images/avatars/notifications-avatar.svg';
import ActiveRoomAvatar from '@assets/images/avatars/room.svg';
import BackArrow from '@assets/images/back-left.svg';
import BankLock from '@assets/images/bank-lock.svg';
import Bank from '@assets/images/bank.svg';
import Basket from '@assets/images/basket.svg';
import BedCircleSlash from '@assets/images/bed-circle-slash.svg';
Expand Down Expand Up @@ -278,6 +279,7 @@ const Expensicons = {
AttachmentNotFound,
BackArrow,
Bank,
BankLock,
Basket,
CircularArrowBackwards,
Bill,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Bestätigen Sie Ihr Konto, um Expensify weiter zu verwenden', subtitle: 'Konto', cta: 'Bestätigen'},
fixFailedBilling: {title: 'Wir konnten Ihre hinterlegte Karte nicht belasten', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Ihr Geschäftskonto wurde gesperrt',
personalTitle: 'Ihr Bankkonto wurde gesperrt',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Geldbörse',
},
},
assignedCards: 'Ihre Expensify Karten',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} verbleibend`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,12 @@ const translations = {
title: "We couldn't bill your card on file",
subtitle: 'Subscription',
},
unlockBankAccount: {
workspaceTitle: 'Your business bank account has been locked',
personalTitle: 'Your bank account has been locked',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Wallet',
},
Comment thread
adamgrzybowski marked this conversation as resolved.
},
freeTrialSection: {
title: ({days}: {days: number}) => `Free trial: ${days} ${days === 1 ? 'day' : 'days'} left!`,
Expand Down
8 changes: 7 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
fixPersonalCardConnection: {
title: ({cardName}: {cardName?: string}) => (cardName ? `Arreglar la conexión de la tarjeta personal de ${cardName}` : 'Arreglar la conexión de la tarjeta personal'),
subtitle: 'Monedero',
subtitle: 'Billetera',
Comment thread
adamgrzybowski marked this conversation as resolved.
},
fixAccountingConnection: {
title: ({integrationName}: {integrationName: string}) => `Reconectar con ${integrationName}`,
Expand Down Expand Up @@ -903,6 +903,12 @@ const translations: TranslationDeepObject<typeof en> = {
title: 'No pudimos cobrar a la tarjeta registrada.',
subtitle: 'Suscripción',
},
unlockBankAccount: {
workspaceTitle: 'Tu cuenta bancaria empresarial ha sido bloqueada',
personalTitle: 'Tu cuenta bancaria ha sido bloqueada',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Billetera',
},
},
freeTrialSection: {
title: ({days}: {days: number}) => `Prueba gratuita: ${days} ${days === 1 ? 'día' : 'días'} restantes!`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Validez votre compte pour continuer à utiliser Expensify', subtitle: 'Compte', cta: 'Valider'},
fixFailedBilling: {title: 'Nous n’avons pas pu débiter votre carte enregistrée', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Votre compte bancaire professionnel a été verrouillé',
personalTitle: 'Votre compte bancaire a été verrouillé',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Portefeuille',
},
},
assignedCards: 'Vos cartes Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restant`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Conferma il tuo account per continuare a usare Expensify', subtitle: 'Account', cta: 'Conferma'},
fixFailedBilling: {title: 'Non abbiamo potuto addebitare la carta salvata nel profilo', subtitle: 'Abbonamento'},
unlockBankAccount: {
workspaceTitle: 'Il conto bancario della tua azienda è stato bloccato',
personalTitle: 'Il tuo conto bancario è stato bloccato',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Portafoglio',
},
},
assignedCards: 'Le tue Carte Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} rimanenti`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,12 @@ const translations: TranslationDeepObject<typeof en> = {
fixPersonalCardConnection: {title: ({cardName}: {cardName?: string}) => (cardName ? `${cardName}個人カードの接続を修正` : '個人カードの連携を修正'), subtitle: 'ウォレット'},
validateAccount: {title: 'Expensify を引き続きご利用いただくには、アカウントを認証してください', subtitle: 'アカウント', cta: '検証する'},
fixFailedBilling: {title: '登録されているカードから請求できませんでした', subtitle: 'サブスクリプション'},
unlockBankAccount: {
workspaceTitle: 'ビジネス用銀行口座がロックされました',
personalTitle: 'あなたの銀行口座はロックされています',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'ウォレット',
},
},
assignedCards: 'お客様の Expensify カード',
assignedCardsRemaining: ({amount}: {amount: string}) => `残額:${amount}`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,6 +958,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Valideer je account om Expensify te blijven gebruiken', subtitle: 'Account', cta: 'Valideren'},
fixFailedBilling: {title: 'We konden je kaart in ons bestand niet belasten', subtitle: 'Abonnement'},
unlockBankAccount: {
workspaceTitle: 'Je zakelijke bankrekening is geblokkeerd',
personalTitle: 'Je bankrekening is vergrendeld',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Portemonnee',
},
},
assignedCards: 'Je Expensify Kaarten',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} resterend`,
Expand Down
6 changes: 6 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Zweryfikuj swoje konto, aby dalej korzystać z Expensify', subtitle: 'Konto', cta: 'Zatwierdź'},
fixFailedBilling: {title: 'Nie mogliśmy obciążyć zapisanej karty', subtitle: 'Subskrypcja'},
unlockBankAccount: {
workspaceTitle: 'Twoje firmowe konto bankowe zostało zablokowane',
personalTitle: 'Twoje konto bankowe zostało zablokowane',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Portfel',
},
},
assignedCards: 'Twoje Karty Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `Pozostało ${amount}`,
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 @@ -957,6 +957,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: 'Valide sua conta para continuar usando o Expensify', subtitle: 'Conta', cta: 'Validar'},
fixFailedBilling: {title: 'Não foi possível cobrar o cartão cadastrado', subtitle: 'Assinatura'},
unlockBankAccount: {
workspaceTitle: 'Sua conta bancária comercial foi bloqueada',
personalTitle: 'Sua conta bancária foi bloqueada',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: 'Carteira',
},
},
assignedCards: 'Seus Cartões Expensify',
assignedCardsRemaining: ({amount}: {amount: string}) => `${amount} restante`,
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 @@ -925,6 +925,12 @@ const translations: TranslationDeepObject<typeof en> = {
},
validateAccount: {title: '验证您的账户以继续使用 Expensify', subtitle: '账户', cta: '验证'},
fixFailedBilling: {title: '我们无法向您档案中的银行卡收费', subtitle: '订阅'},
unlockBankAccount: {
workspaceTitle: '您的企业银行账户已被锁定',
personalTitle: '您的银行账户已被锁定',
workspaceSubtitle: ({policyName}: {policyName: string}) => policyName,
personalSubtitle: '钱包',
},
},
assignedCards: '你的 Expensify 卡',
assignedCardsRemaining: ({amount}: {amount: string}) => `剩余 ${amount}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {primaryLoginSelector} from '@selectors/Account';
import useOnyx from '@hooks/useOnyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {BankAccountList, Policy} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';

type LockedBankAccount = {
/** Stable key used to render this account widget */
key: string;

/** The ID of the locked bank account */
bankAccountID: number;

/** The policy name — undefined means personal account */
policyName?: string;
};

function useTimeSensitiveLockedBankAccount(adminPolicies: Policy[] | undefined) {
Comment thread
adamgrzybowski marked this conversation as resolved.
const [bankAccountList = getEmptyObject<BankAccountList>()] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [primaryLogin] = useOnyx(ONYXKEYS.ACCOUNT, {selector: primaryLoginSelector});
const lockedBankAccounts: LockedBankAccount[] = [];
const workspaceLockedBankAccountIDs = new Set<number>();

for (const policy of adminPolicies ?? []) {
const achAccount = policy.achAccount;
if (achAccount?.state !== CONST.BANK_ACCOUNT.STATE.LOCKED || !achAccount.bankAccountID) {
continue;
}

const isCurrentUserReimburser = !!primaryLogin && achAccount.reimburser === primaryLogin;
if (!isCurrentUserReimburser) {
continue;
}

workspaceLockedBankAccountIDs.add(achAccount.bankAccountID);
lockedBankAccounts.push({
key: `workspace-${policy.id}-${achAccount.bankAccountID}`,
bankAccountID: achAccount.bankAccountID,
policyName: policy.name,
});
}

for (const account of Object.values(bankAccountList ?? {})) {
const {bankAccountID, state, type} = account?.accountData ?? {};
const isPersonalAccount = type === undefined || type === CONST.BANK_ACCOUNT.TYPE.PERSONAL;

if (!isPersonalAccount) {
continue;
}

if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !workspaceLockedBankAccountIDs.has(bankAccountID)) {
lockedBankAccounts.push({
key: `personal-${bankAccountID}`,
bankAccountID,
});
}
}

return {
lockedBankAccounts,
};
}

export default useTimeSensitiveLockedBankAccount;
26 changes: 20 additions & 6 deletions src/pages/home/TimeSensitiveSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {ConnectionName, PolicyConnectionName} from '@src/types/onyx/Policy'
import useTimeSensitiveAddPaymentCard from './hooks/useTimeSensitiveAddPaymentCard';
import useTimeSensitiveBilling from './hooks/useTimeSensitiveBilling';
import useTimeSensitiveCards from './hooks/useTimeSensitiveCards';
import useTimeSensitiveLockedBankAccount from './hooks/useTimeSensitiveLockedBankAccount';
import ActivateCard from './items/ActivateCard';
import AddPaymentCard from './items/AddPaymentCard';
import AddShippingAddress from './items/AddShippingAddress';
Expand All @@ -28,6 +29,7 @@ import FixCompanyCardConnection from './items/FixCompanyCardConnection';
import FixFailedBilling from './items/FixFailedBilling';
import FixPersonalCardConnection from './items/FixPersonalCardConnection';
import ReviewCardFraud from './items/ReviewCardFraud';
import UnlockBankAccount from './items/UnlockBankAccount';
import ValidateAccount from './items/ValidateAccount';

type BrokenAccountingConnection = {
Expand Down Expand Up @@ -80,6 +82,7 @@ function TimeSensitiveSection() {
});
const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
const {lockedBankAccounts} = useTimeSensitiveLockedBankAccount(adminPolicies);

// Get card feed errors for company card connections (Release 4)
const cardFeedErrors = useCardFeedErrors();
Expand Down Expand Up @@ -153,6 +156,7 @@ function TimeSensitiveSection() {
// If a widget has additional conditions in the render (e.g. && !!discountInfo), those
// must be reflected here to avoid showing an empty "Time sensitive" section.
const hasAnyTimeSensitiveContent =
lockedBankAccounts.length > 0 ||
shouldShowValidateAccount ||
shouldShowFixFailedBilling ||
shouldShowReviewCardFraud ||
Expand All @@ -174,9 +178,10 @@ function TimeSensitiveSection() {
// 4. Add payment card (trial ended, no payment card)
// 5. Broken bank connections (company cards)
// 6. Broken bank connections (personal cards)
// 7. Broken accounting connections
// 8. Expensify card shipping
// 9. Expensify card activation
// 7. Locked bank accounts (workspace VBAs and personal)
// 8. Broken accounting connections
// 9. Expensify card shipping
// 10. Expensify card activation
return (
<WidgetContainer title={translate('homePage.timeSensitiveSection.title')}>
<View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
Expand Down Expand Up @@ -231,7 +236,16 @@ function TimeSensitiveSection() {
);
})}

{/* Priority 7: Broken accounting connections */}
{/* Priority 7: Locked bank accounts */}
{lockedBankAccounts.map((lockedBankAccount) => (
<UnlockBankAccount
key={lockedBankAccount.key}
bankAccountID={lockedBankAccount.bankAccountID}
policyName={lockedBankAccount.policyName}
/>
))}

{/* Priority 8: Broken accounting connections */}
{brokenAccountingConnections.map((connection) => (
<FixAccountingConnection
key={`accounting-${connection.policyID}-${connection.connectionName}`}
Expand All @@ -241,7 +255,7 @@ function TimeSensitiveSection() {
/>
))}

{/* Priority 8: Expensify card shipping */}
{/* Priority 9: Expensify card shipping */}
{shouldShowAddShippingAddress &&
cardsNeedingShippingAddress.map((card) => (
<AddShippingAddress
Expand All @@ -250,7 +264,7 @@ function TimeSensitiveSection() {
/>
))}

{/* Priority 9: Expensify card activation */}
{/* Priority 10: Expensify card activation */}
{shouldShowActivateCard &&
cardsNeedingActivation.map((card) => (
<ActivateCard
Expand Down
57 changes: 57 additions & 0 deletions src/pages/home/TimeSensitiveSection/items/UnlockBankAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {hasSeenTourSelector} from '@selectors/Onboarding';
import React from 'react';
import BaseWidgetItem from '@components/BaseWidgetItem';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDelegateAccountID from '@hooks/useDelegateAccountID';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {pressLockedBankAccount} from '@libs/actions/BankAccounts';
import {navigateToConciergeChat} from '@libs/actions/Report';
import colors from '@styles/theme/colors';
import ONYXKEYS from '@src/ONYXKEYS';

type UnlockBankAccountProps = {
/** The ID of the locked bank account */
bankAccountID: number;

/** The policy name — undefined means personal account (subtitle: 'Wallet') */
policyName?: string;
};

function UnlockBankAccount({bankAccountID, policyName}: UnlockBankAccountProps) {
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['BankLock']);
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [betas] = useOnyx(ONYXKEYS.BETAS);
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const delegateAccountID = useDelegateAccountID();

const title = policyName ? translate('homePage.timeSensitiveSection.unlockBankAccount.workspaceTitle') : translate('homePage.timeSensitiveSection.unlockBankAccount.personalTitle');

const subtitle = policyName
? translate('homePage.timeSensitiveSection.unlockBankAccount.workspaceSubtitle', {policyName})
: translate('homePage.timeSensitiveSection.unlockBankAccount.personalSubtitle');

const handleCtaPress = () => {
pressLockedBankAccount(bankAccountID, translate, conciergeReportID, delegateAccountID);
navigateToConciergeChat(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas);
};

return (
<BaseWidgetItem
icon={icons.BankLock}
iconBackgroundColor={colors.tangerine100}
iconFill={colors.tangerine500}
title={title}
subtitle={subtitle}
ctaText={translate('homePage.timeSensitiveSection.ctaFix')}
onCtaPress={handleCtaPress}
buttonProps={{danger: true}}
/>
);
}

export default UnlockBankAccount;
Loading
Loading