diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg
index 351b1cf86d389..436bdce844c99 100644
--- a/assets/images/product-illustrations/broken-magnifying-glass.svg
+++ b/assets/images/product-illustrations/broken-magnifying-glass.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index a4281c1ad1fae..16846c1c8295f 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -3554,6 +3554,8 @@ const CONST = {
COMPANY_CARDS: {
BROKEN_CONNECTION_IGNORED_STATUSES: brokenConnectionScrapeStatuses,
CONNECTION_ERROR: 'connectionError',
+ WORKSPACE_FEEDS_LOAD_ERROR: 'workspaceFeedsLoadError',
+ FEED_LOAD_ERROR: 'feedLoadError',
STEP: {
SELECT_BANK: 'SelectBank',
SELECT_FEED_TYPE: 'SelectFeedType',
diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx
index c822a4605c796..88f4207da81e8 100644
--- a/src/components/BlockingViews/BlockingView.tsx
+++ b/src/components/BlockingViews/BlockingView.tsx
@@ -152,7 +152,7 @@ function BlockingView({
/>
)}
- {title}
+ {title}
{CustomSubtitle}
{!CustomSubtitle && (
diff --git a/src/components/BlockingViews/FullPageErrorView.tsx b/src/components/BlockingViews/FullPageErrorView.tsx
index 5cd9d28fb25a3..a40e510c57cc0 100644
--- a/src/components/BlockingViews/FullPageErrorView.tsx
+++ b/src/components/BlockingViews/FullPageErrorView.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import {View} from 'react-native';
-import type {StyleProp, TextStyle} from 'react-native';
+import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
@@ -28,10 +28,12 @@ type FullPageErrorViewProps = {
/** The style of the subtitle message */
subtitleStyle?: StyleProp;
+
+ containerStyle?: StyleProp;
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function FullPageErrorView({testID, children = null, shouldShow = false, title = '', subtitle = '', shouldForceFullScreen = false, subtitleStyle}: FullPageErrorViewProps) {
+function FullPageErrorView({testID, children = null, shouldShow = false, title = '', subtitle = '', shouldForceFullScreen = false, subtitleStyle, containerStyle}: FullPageErrorViewProps) {
const styles = useThemeStyles();
const illustrations = useMemoizedLazyIllustrations(['BrokenMagnifyingGlass']);
@@ -39,7 +41,7 @@ function FullPageErrorView({testID, children = null, shouldShow = false, title =
return (
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index b08800ec89c4c..e50791de056cd 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -1093,6 +1093,7 @@ function Search({
{
- openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID);
+ openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID, translate);
};
const {isOffline} = useNetwork({onReconnect: fetchCompanyCards});
diff --git a/src/hooks/useCardFeeds.tsx b/src/hooks/useCardFeeds.tsx
index 473b938fcc9f5..a214aa3905454 100644
--- a/src/hooks/useCardFeeds.tsx
+++ b/src/hooks/useCardFeeds.tsx
@@ -1,7 +1,7 @@
import type {OnyxCollection, ResultMetadata} from 'react-native-onyx';
-import {getCombinedCardFeedsFromAllFeeds} from '@libs/CardFeedUtils';
+import {getCombinedCardFeedsFromAllFeeds, getWorkspaceCardFeedsStatus} from '@libs/CardFeedUtils';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {CardFeeds, CombinedCardFeed, CombinedCardFeeds, CompanyCardFeedWithDomainID} from '@src/types/onyx';
+import type {CardFeeds, CardFeedsStatusByDomainID, CombinedCardFeed, CombinedCardFeeds, CompanyCardFeedWithDomainID} from '@src/types/onyx';
import useOnyx from './useOnyx';
import useWorkspaceAccountID from './useWorkspaceAccountID';
@@ -19,7 +19,7 @@ import useWorkspaceAccountID from './useWorkspaceAccountID';
* 2. The result metadata from the Onyx collection fetch.
* 3. Card feeds specific to the given policyID (or `undefined` if unavailable).
*/
-const useCardFeeds = (policyID: string | undefined): [CombinedCardFeeds | undefined, ResultMetadata>, CardFeeds | undefined] => {
+const useCardFeeds = (policyID: string | undefined): [CombinedCardFeeds | undefined, ResultMetadata>, CardFeeds | undefined, CardFeedsStatusByDomainID] => {
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [allFeeds, allFeedsResult] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});
const defaultFeed = allFeeds?.[`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`];
@@ -31,7 +31,9 @@ const useCardFeeds = (policyID: string | undefined): [CombinedCardFeeds | undefi
workspaceFeeds = getCombinedCardFeedsFromAllFeeds(allFeeds, shouldIncludeFeedPredicate);
}
- return [workspaceFeeds, allFeedsResult, defaultFeed];
+ const workspaceCardFeedsStatus = getWorkspaceCardFeedsStatus(allFeeds);
+
+ return [workspaceFeeds, allFeedsResult, defaultFeed, workspaceCardFeedsStatus];
};
export default useCardFeeds;
diff --git a/src/hooks/useCompanyCards.ts b/src/hooks/useCompanyCards.ts
index 7975b56f90ec8..32e0f51194342 100644
--- a/src/hooks/useCompanyCards.ts
+++ b/src/hooks/useCompanyCards.ts
@@ -5,7 +5,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CardFeeds, CardList} from '@src/types/onyx';
import type {AssignableCardsList, WorkspaceCardsList} from '@src/types/onyx/Card';
-import type {CombinedCardFeeds, CompanyCardFeed, CompanyCardFeedWithDomainID, CompanyFeeds} from '@src/types/onyx/CardFeeds';
+import type {CardFeedsStatusByDomainID, CombinedCardFeeds, CompanyCardFeed, CompanyCardFeedWithDomainID, CompanyFeeds} from '@src/types/onyx/CardFeeds';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import useCardFeeds from './useCardFeeds';
import type {CombinedCardFeed} from './useCardFeeds';
@@ -25,6 +25,7 @@ type UseCompanyCardsResult = Partial<{
feedName: CompanyCardFeedWithDomainID;
cardList: AssignableCardsList;
assignedCards: CardList;
+ workspaceCardFeedsStatus: CardFeedsStatusByDomainID;
cardNames: string[];
allCardFeeds: CombinedCardFeeds;
companyCardFeeds: CompanyFeeds;
@@ -48,7 +49,7 @@ function useCompanyCards({policyID, feedName: feedNameProp}: UseCompanyCardsProp
const policyIDKey = policyID || CONST.DEFAULT_MISSING_ID;
const [lastSelectedFeed, lastSelectedFeedMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyIDKey}`, {canBeMissing: true});
- const [allCardFeeds, allCardFeedsMetadata] = useCardFeeds(policyID);
+ const [allCardFeeds, allCardFeedsMetadata, , workspaceCardFeedsStatus] = useCardFeeds(policyID);
const feedName = feedNameProp ?? getSelectedFeed(lastSelectedFeed, allCardFeeds);
const bankName = feedName ? getCompanyCardFeed(feedName) : undefined;
@@ -90,6 +91,7 @@ function useCompanyCards({policyID, feedName: feedNameProp}: UseCompanyCardsProp
companyCardFeeds,
cardList,
assignedCards,
+ workspaceCardFeedsStatus,
cardNames,
selectedFeed,
bankName,
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 9dbe2a90e6014..42369cf3e3b9b 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -781,7 +781,7 @@ const translations: TranslationDeepObject = {
expiredCodeDescription: 'Gehe zurück zum ursprünglichen Gerät und fordere einen neuen Code an',
successfulNewCodeRequest: 'Code angefordert. Bitte überprüfe dein Gerät.',
tfaRequiredTitle: dedent(`
- Zwei-Faktor-Authentifizierung
+ Zwei-Faktor-Authentifizierung
erforderlich
`),
tfaRequiredDescription: dedent(`
@@ -4896,6 +4896,14 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
assignCardFailedError: 'Kartenzuweisung fehlgeschlagen.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Aufhebung der Kartenzuweisung fehlgeschlagen.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Kartenfeeds konnten nicht geladen werden',
+ workspaceFeedsCouldNotBeLoadedMessage:
+ 'Beim Laden der Workspace-Kartenfeeds ist ein Fehler aufgetreten. Bitte versuche es erneut oder wende dich an deine Administratorin bzw. deinen Administrator.',
+ feedCouldNotBeLoadedTitle: 'Dieser Feed konnte nicht geladen werden',
+ feedCouldNotBeLoadedMessage: 'Beim Laden dieses Feeds ist ein Fehler aufgetreten. Bitte versuche es erneut oder kontaktiere deine Administratorin/deinen Administrator.',
+ tryAgain: 'Erneut versuchen',
+ },
},
expensifyCard: {
issueAndManageCards: 'Expensify Cards ausstellen und verwalten',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c5025b2ffeffc..7f9e0265f4409 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -4706,6 +4706,13 @@ const translations = {
companyCards: {
addCards: 'Add cards',
selectCards: 'Select cards',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: "Couldn't load card feeds",
+ workspaceFeedsCouldNotBeLoadedMessage: 'An error occurred while loading workspace card feeds. Please try again or contact your administrator.',
+ feedCouldNotBeLoadedTitle: "Couldn't load this feed",
+ feedCouldNotBeLoadedMessage: 'An error occurred while loading this feed. Please try again or contact your administrator.',
+ tryAgain: 'Try again',
+ },
addNewCard: {
other: 'Other',
cardProviders: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c8961f508b267..ddaa7f4f720be 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -4451,6 +4451,14 @@ ${amount} para ${merchant} - ${date}`,
companyCards: {
addCards: 'Añadir tarjetas',
selectCards: 'Seleccionar tarjetas',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Error al cargar las fuentes de tarjetas del espacio de trabajo',
+ workspaceFeedsCouldNotBeLoadedMessage:
+ 'Ocurrió un error al cargar las fuentes de tarjetas del espacio de trabajo. Por favor, inténtelo de nuevo o contacte a su administrador.',
+ feedCouldNotBeLoadedTitle: 'Error al cargar esta fuente de tarjetas',
+ feedCouldNotBeLoadedMessage: 'Ocurrió un error al cargar esta fuente de tarjetas. Por favor, inténtelo de nuevo o contacte a su administrador.',
+ tryAgain: 'Inténtalo de nuevo',
+ },
addNewCard: {
other: 'Otros',
cardProviders: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 8e5ea4c8f31b2..0795753cb5d95 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -784,7 +784,7 @@ const translations: TranslationDeepObject = {
expiredCodeDescription: 'Retournez sur l’appareil d’origine et demandez un nouveau code',
successfulNewCodeRequest: 'Code demandé. Veuillez vérifier votre appareil.',
tfaRequiredTitle: dedent(`
- Authentification à deux facteurs
+ Authentification à deux facteurs
requise
`),
tfaRequiredDescription: dedent(`
@@ -4648,7 +4648,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
_Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
customSegmentScriptIDTitle: 'Quel est l’ID du script ?',
- customSegmentScriptIDFooter: `Vous pouvez trouver les ID de script de segment personnalisé dans NetSuite sous :
+ customSegmentScriptIDFooter: `Vous pouvez trouver les ID de script de segment personnalisé dans NetSuite sous :
1. *Customization > Lists, Records, & Fields > Custom Segments*.
2. Cliquez sur un segment personnalisé.
@@ -4902,6 +4902,14 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
assignCardFailedError: 'L’attribution de la carte a échoué.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Échec de la désaffectation de la carte.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Impossible de charger les flux de carte',
+ workspaceFeedsCouldNotBeLoadedMessage:
+ 'Une erreur s’est produite lors du chargement des flux de cartes de l’espace de travail. Veuillez réessayer ou contacter votre administrateur.',
+ feedCouldNotBeLoadedTitle: 'Impossible de charger ce flux',
+ feedCouldNotBeLoadedMessage: 'Une erreur s’est produite lors du chargement de ce flux. Veuillez réessayer ou contacter votre administrateur.',
+ tryAgain: 'Réessayer',
+ },
},
expensifyCard: {
issueAndManageCards: 'Émettre et gérer vos cartes Expensify',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index ee09a7f6314eb..6165daf3201d4 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -782,7 +782,7 @@ const translations: TranslationDeepObject = {
expiredCodeDescription: 'Torna al dispositivo originale e richiedi un nuovo codice',
successfulNewCodeRequest: 'Codice richiesto. Controlla il tuo dispositivo.',
tfaRequiredTitle: dedent(`
- Autenticazione a due fattori
+ Autenticazione a due fattori
richiesta
`),
tfaRequiredDescription: dedent(`
@@ -4882,6 +4882,14 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
assignCardFailedError: 'Assegnazione della carta non riuscita.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Rimozione della carta non riuscita.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Impossibile caricare i feed delle carte',
+ workspaceFeedsCouldNotBeLoadedMessage:
+ 'Si è verificato un errore durante il caricamento dei feed della scheda dell’area di lavoro. Riprova o contatta il tuo amministratore.',
+ feedCouldNotBeLoadedTitle: 'Impossibile caricare questo feed',
+ feedCouldNotBeLoadedMessage: 'Si è verificato un errore durante il caricamento di questo feed. Riprova o contatta il tuo amministratore.',
+ tryAgain: 'Riprova',
+ },
},
expensifyCard: {
issueAndManageCards: 'Emetti e gestisci le tue Expensify Card',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index a4aecc65c5324..e3f03bcb919e5 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -4855,6 +4855,13 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
assignCardFailedError: 'カードの割り当てに失敗しました。',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'カードの割り当て解除に失敗しました。',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'カードフィードを読み込めませんでした',
+ workspaceFeedsCouldNotBeLoadedMessage: 'ワークスペースのカードフィードを読み込む際にエラーが発生しました。もう一度お試しいただくか、管理者に連絡してください。',
+ feedCouldNotBeLoadedTitle: 'このフィードを読み込めませんでした',
+ feedCouldNotBeLoadedMessage: 'このフィードの読み込み中にエラーが発生しました。もう一度お試しいただくか、管理者に連絡してください。',
+ tryAgain: '再試行',
+ },
},
expensifyCard: {
issueAndManageCards: 'Expensify カードの発行と管理',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 5b0db59e1cded..118f76ec63785 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -4627,7 +4627,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze helppagina](${CONST.NETSUITE
_Voor meer gedetailleerde instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
customSegmentScriptIDTitle: 'Wat is de script-ID?',
- customSegmentScriptIDFooter: `Je kunt aangepaste segmentscript-ID’s in NetSuite vinden onder:
+ customSegmentScriptIDFooter: `Je kunt aangepaste segmentscript-ID’s in NetSuite vinden onder:
1. *Customization > Lists, Records, & Fields > Custom Segments*.
2. Klik op een aangepast segment.
@@ -4878,6 +4878,14 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO
assignCardFailedError: 'Toewijzing van kaart mislukt.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Kaartontkoppeling mislukt.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Kan kaartfeeds niet laden',
+ workspaceFeedsCouldNotBeLoadedMessage:
+ 'Er is een fout opgetreden bij het laden van de kaartfeeds van de werkruimte. Probeer het opnieuw of neem contact op met uw beheerder.',
+ feedCouldNotBeLoadedTitle: 'Kon deze feed niet laden',
+ feedCouldNotBeLoadedMessage: 'Er is een fout opgetreden bij het laden van deze feed. Probeer het opnieuw of neem contact op met uw beheerder.',
+ tryAgain: 'Opnieuw proberen',
+ },
},
expensifyCard: {
issueAndManageCards: 'Uw Expensify Cards uitgeven en beheren',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index dea21683527cd..530041f50ba90 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -782,7 +782,7 @@ const translations: TranslationDeepObject = {
expiredCodeDescription: 'Wróć do oryginalnego urządzenia i poproś o nowy kod',
successfulNewCodeRequest: 'Kod został wysłany. Sprawdź swoje urządzenie.',
tfaRequiredTitle: dedent(`
- Dwuskładnikowe uwierzytelnianie
+ Dwuskładnikowe uwierzytelnianie
wymagane
`),
tfaRequiredDescription: dedent(`
@@ -4867,6 +4867,13 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
assignCardFailedError: 'Przypisanie karty nie powiodło się.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Nie udało się odłączyć karty.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Nie można było wczytać kanałów kart',
+ workspaceFeedsCouldNotBeLoadedMessage: 'Wystąpił błąd podczas ładowania kanałów kart w przestrzeni roboczej. Spróbuj ponownie lub skontaktuj się z administratorem.',
+ feedCouldNotBeLoadedTitle: 'Nie można było wczytać tego kanału',
+ feedCouldNotBeLoadedMessage: 'Wystąpił błąd podczas ładowania tego kanału. Spróbuj ponownie lub skontaktuj się ze swoim administratorem.',
+ tryAgain: 'Spróbuj ponownie',
+ },
},
expensifyCard: {
issueAndManageCards: 'Wydawaj i zarządzaj swoimi kartami Expensify',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index a4c33c4793ef1..fa17afb090711 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -4867,6 +4867,13 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT
assignCardFailedError: 'Falha na atribuição do cartão.',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: 'Falha ao desatribuir o cartão.',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: 'Não foi possível carregar os feeds do cartão',
+ workspaceFeedsCouldNotBeLoadedMessage: 'Ocorreu um erro ao carregar os feeds de cartões do workspace. Tente novamente ou entre em contato com o administrador.',
+ feedCouldNotBeLoadedTitle: 'Não foi possível carregar este feed',
+ feedCouldNotBeLoadedMessage: 'Ocorreu um erro ao carregar este feed. Tente novamente ou entre em contato com o seu administrador.',
+ tryAgain: 'Tentar novamente',
+ },
},
expensifyCard: {
issueAndManageCards: 'Emitir e gerenciar seus Cartões Expensify',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index cefb15fbbc51c..ea9543849e78f 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -767,7 +767,7 @@ const translations: TranslationDeepObject = {
请输入在最初请求该代码的设备上显示的代码
`),
doNotShare: dedent(`
- 不要与任何人分享你的验证码。
+ 不要与任何人分享你的验证码。
Expensify 永远不会向你索要它!
`),
or: ',或',
@@ -776,7 +776,7 @@ const translations: TranslationDeepObject = {
expiredCodeDescription: '返回原始设备并请求新验证码',
successfulNewCodeRequest: '已请求验证码。请检查您的设备。',
tfaRequiredTitle: dedent(`
- 双重身份验证
+ 双重身份验证
必填
`),
tfaRequiredDescription: dedent(`
@@ -4776,6 +4776,13 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
assignCardFailedError: '卡片分配失败。',
cardAlreadyAssignedError: 'This card is already assigned to a user in another workspace.',
unassignCardFailedError: '卡片取消分配失败。',
+ error: {
+ workspaceFeedsCouldNotBeLoadedTitle: '无法加载卡片信息流',
+ workspaceFeedsCouldNotBeLoadedMessage: '加载工作区卡片动态时出错。请重试或联系您的管理员。',
+ feedCouldNotBeLoadedTitle: '无法加载此订阅内容',
+ feedCouldNotBeLoadedMessage: '加载此信息流时出错。请重试或联系您的管理员。',
+ tryAgain: '重试',
+ },
},
expensifyCard: {
issueAndManageCards: '发放和管理您的 Expensify 卡',
diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts
index d2bc5e88d16ac..6a28efbb790ce 100644
--- a/src/libs/CardFeedUtils.ts
+++ b/src/libs/CardFeedUtils.ts
@@ -6,7 +6,7 @@ import CONST from '@src/CONST';
import type {CombinedCardFeeds} from '@src/hooks/useCardFeeds';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx';
-import type {CombinedCardFeed} from '@src/types/onyx/CardFeeds';
+import type {CardFeedsStatus, CardFeedsStatusByDomainID, CombinedCardFeed} from '@src/types/onyx/CardFeeds';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {
getBankName,
@@ -515,6 +515,21 @@ function getCardFeedsForDisplayPerPolicy(allCardFeeds: OnyxCollection
return cardFeedsForDisplayPerPolicy;
}
+function getCardFeedStatus(feed: CardFeeds | undefined): CardFeedsStatus {
+ return {
+ errors: feed?.errors,
+ isLoading: feed?.isLoading,
+ };
+}
+
+function getWorkspaceCardFeedsStatus(allFeeds: OnyxCollection | undefined): CardFeedsStatusByDomainID {
+ return Object.entries(allFeeds ?? {}).reduce((acc, [onyxKey, feeds]) => {
+ const domainID = Number(onyxKey.split('_').at(-1));
+ acc[domainID] = getCardFeedStatus(feeds);
+ return acc;
+ }, {} as CardFeedsStatusByDomainID);
+}
+
function getCombinedCardFeedsFromAllFeeds(allFeeds: OnyxCollection | undefined, includeFeedPredicate?: (feed: CombinedCardFeed) => boolean): CombinedCardFeeds {
return Object.entries(allFeeds ?? {}).reduce((acc, [onyxKey, feeds]) => {
const domainID = Number(onyxKey.split('_').at(-1));
@@ -530,17 +545,19 @@ function getCombinedCardFeedsFromAllFeeds(allFeeds: OnyxCollection |
const feedSettings = companyCards?.[feedName];
const oAuthAccountDetails = workspaceFeedsSettings?.oAuthAccountDetails?.[feedName];
const customFeedName = workspaceFeedsSettings?.companyCardNicknames?.[feedName];
+ const status = workspaceFeedsSettings?.cardFeedsStatus?.[feedName];
if (!domainID) {
continue;
}
- const combinedCardFeed = {
+ const combinedCardFeed: CombinedCardFeed = {
...feedSettings,
...oAuthAccountDetails,
customFeedName,
domainID,
feed: feedName,
+ status,
};
if (includeFeedPredicate && !includeFeedPredicate(combinedCardFeed)) {
@@ -571,4 +588,6 @@ export {
getCardFeedsForDisplay,
getCardFeedsForDisplayPerPolicy,
getCombinedCardFeedsFromAllFeeds,
+ getCardFeedStatus,
+ getWorkspaceCardFeedsStatus,
};
diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts
index 46c5a1f661427..17e5eb284340a 100644
--- a/src/libs/actions/CompanyCards.ts
+++ b/src/libs/actions/CompanyCards.ts
@@ -839,13 +839,14 @@ function clearCompanyCardErrorField(domainOrWorkspaceAccountID: number, cardID:
});
}
-function openPolicyCompanyCardsPage(policyID: string, domainOrWorkspaceAccountID: number) {
+function openPolicyCompanyCardsPage(policyID: string, domainOrWorkspaceAccountID: number, translate: LocaleContextProps['translate']) {
const optimisticData: Array> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`,
value: {
isLoading: true,
+ errors: null,
},
},
];
@@ -856,6 +857,9 @@ function openPolicyCompanyCardsPage(policyID: string, domainOrWorkspaceAccountID
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`,
value: {
isLoading: false,
+ errors: {
+ [CONST.COMPANY_CARDS.WORKSPACE_FEEDS_LOAD_ERROR]: translate('workspace.companyCards.error.workspaceFeedsCouldNotBeLoadedMessage'),
+ },
},
},
];
@@ -867,14 +871,66 @@ function openPolicyCompanyCardsPage(policyID: string, domainOrWorkspaceAccountID
API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_PAGE, params, {optimisticData, finallyData});
}
-function openPolicyCompanyCardsFeed(domainAccountID: number, policyID: string, feed: CompanyCardFeedWithNumber) {
+function openPolicyCompanyCardsFeed(domainAccountID: number, policyID: string, feed: CompanyCardFeedWithNumber, translate: LocaleContextProps['translate']) {
const parameters: OpenPolicyCompanyCardsFeedParams = {
domainAccountID,
policyID,
feed,
};
- API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters);
+ const optimisticData: Array> = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
+ value: {
+ settings: {
+ cardFeedsStatus: {
+ [feed]: {
+ isLoading: true,
+ errors: null,
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: Array> = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
+ value: {
+ settings: {
+ cardFeedsStatus: {
+ [feed]: {
+ isLoading: false,
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: Array> = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
+ value: {
+ settings: {
+ cardFeedsStatus: {
+ [feed]: {
+ isLoading: false,
+ errors: {
+ [CONST.COMPANY_CARDS.FEED_LOAD_ERROR]: translate('workspace.companyCards.error.feedCouldNotBeLoadedMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters, {optimisticData, successData, failureData});
}
function openAssignFeedCardPage(policyID: string, feed: CompanyCardFeed, domainOrWorkspaceAccountID: number) {
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
index bcffcee6ddf35..d39380d64eb24 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useState} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
import DecisionModal from '@components/DecisionModal';
import useAssignCard from '@hooks/useAssignCard';
import useCompanyCards from '@hooks/useCompanyCards';
@@ -44,23 +44,32 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeed);
const {isOffline} = useNetwork({
- onReconnect: () => openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID),
+ onReconnect: () => openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID, translate),
});
const isLoading = !isOffline && (!allCardFeeds || (isFeedAdded && isLoadingOnyxValue(cardListMetadata)));
- useEffect(() => {
- openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID);
- }, [policyID, domainOrWorkspaceAccountID]);
+
+ const loadPolicyCompanyCardsPage = useCallback(() => {
+ openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID, translate);
+ }, [domainOrWorkspaceAccountID, policyID, translate]);
useEffect(() => {
+ loadPolicyCompanyCardsPage();
+ }, [policyID, domainOrWorkspaceAccountID, loadPolicyCompanyCardsPage]);
+
+ const loadPolicyCompanyCardsFeed = useCallback(() => {
if (isLoading || !bankName || isFeedPending) {
return;
}
const clientMemberEmails = Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList));
openWorkspaceMembersPage(policyID, clientMemberEmails);
- openPolicyCompanyCardsFeed(domainOrWorkspaceAccountID, policyID, bankName);
- }, [bankName, isLoading, policyID, isFeedPending, domainOrWorkspaceAccountID, policy?.employeeList]);
+ openPolicyCompanyCardsFeed(domainOrWorkspaceAccountID, policyID, bankName, translate);
+ }, [bankName, domainOrWorkspaceAccountID, isFeedPending, isLoading, policy?.employeeList, policyID, translate]);
+
+ useEffect(() => {
+ loadPolicyCompanyCardsFeed();
+ }, [loadPolicyCompanyCardsFeed]);
const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false);
const {assignCard, isAssigningCardDisabled} = useAssignCard({feedName, policyID, setShouldShowOfflineModal});
@@ -85,6 +94,8 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
companyCards={companyCards}
onAssignCard={assignCard}
isAssigningCardDisabled={isAssigningCardDisabled}
+ onReloadPage={loadPolicyCompanyCardsPage}
+ onReloadFeed={loadPolicyCompanyCardsFeed}
/>
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx
index 8599f5ec8c962..ba734b633e167 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx
@@ -1,6 +1,8 @@
import type {ListRenderItemInfo} from '@shopify/flash-list';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import Button from '@components/Button';
import CardFeedIcon from '@components/CardFeedIcon';
import ScrollView from '@components/ScrollView';
import TableRowSkeleton from '@components/Skeletons/TableRowSkeleton';
@@ -8,6 +10,7 @@ import Table from '@components/Table';
import type {ActiveSorting, CompareItemsCallback, FilterConfig, IsItemInFilterCallback, IsItemInSearchCallback, TableColumn, TableHandle} from '@components/Table';
import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import type {UseCompanyCardsResult} from '@hooks/useCompanyCards';
+import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
@@ -43,14 +46,29 @@ type WorkspaceCompanyCardsTableProps = {
/** Company cards */
companyCards: UseCompanyCardsResult;
+ /** Whether to disable assign card button */
+ isAssigningCardDisabled: boolean;
+
/** On assign card callback */
onAssignCard: (cardID: string, encryptedCardNumber: string) => void;
- /** Whether to disable assign card button */
- isAssigningCardDisabled: boolean;
+ /** On reload page callback */
+ onReloadPage: () => void;
+
+ /** On reload feed callback */
+ onReloadFeed: () => void;
};
-function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspaceAccountID, companyCards, onAssignCard, isAssigningCardDisabled}: WorkspaceCompanyCardsTableProps) {
+function WorkspaceCompanyCardsTable({
+ policyID,
+ isPolicyLoaded,
+ domainOrWorkspaceAccountID,
+ companyCards,
+ onAssignCard,
+ isAssigningCardDisabled,
+ onReloadPage,
+ onReloadFeed,
+}: WorkspaceCompanyCardsTableProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();
const {translate, localeCompare} = useLocalize();
@@ -61,6 +79,7 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace
bankName,
cardList,
assignedCards,
+ workspaceCardFeedsStatus,
cardNames,
cardFeedType,
selectedFeed,
@@ -77,14 +96,34 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace
const [failedCompanyCardAssignments] = useOnyx(`${ONYXKEYS.COLLECTION.FAILED_COMPANY_CARDS_ASSIGNMENTS}${domainOrWorkspaceAccountID}_${feedName ?? ''}`, {canBeMissing: true});
const hasNoAssignedCard = Object.keys(assignedCards ?? {}).length === 0;
- const isLoadingFeed = (!feedName && isInitiallyLoadingFeeds) || !isPolicyLoaded || isLoadingOnyxValue(lastSelectedFeedMetadata);
+ const areWorkspaceCardFeedsLoading = !!workspaceCardFeedsStatus?.[domainOrWorkspaceAccountID]?.isLoading;
+ const workspaceCardFeedsErrors = workspaceCardFeedsStatus?.[domainOrWorkspaceAccountID]?.errors;
+
+ const selectedFeedStatus = selectedFeed?.status;
+ const selectedFeedErrors = selectedFeedStatus?.errors;
+
+ const [feedErrorKey, feedErrorMessage] = Object.entries(workspaceCardFeedsErrors ?? selectedFeedErrors ?? {}).at(0) ?? [];
+ const hasFeedErrors = !!feedErrorKey;
+
+ let feedErrorTitle: string | undefined;
+ let feedErrorReloadAction: (() => void) | undefined;
+ if (feedErrorKey === CONST.COMPANY_CARDS.WORKSPACE_FEEDS_LOAD_ERROR) {
+ feedErrorTitle = translate('workspace.companyCards.error.workspaceFeedsCouldNotBeLoadedTitle');
+ feedErrorReloadAction = onReloadPage;
+ } else if (feedErrorKey === CONST.COMPANY_CARDS.FEED_LOAD_ERROR) {
+ feedErrorTitle = translate('workspace.companyCards.error.feedCouldNotBeLoadedTitle');
+ feedErrorReloadAction = onReloadFeed;
+ }
+
+ const isLoadingFeed = (!feedName && isInitiallyLoadingFeeds) || !isPolicyLoaded || isLoadingOnyxValue(lastSelectedFeedMetadata) || !!selectedFeedStatus?.isLoading;
const isLoadingCards = cardFeedType === 'directFeed' ? selectedFeed?.accountList === undefined : isLoadingOnyxValue(cardListMetadata) || cardList === undefined;
+ const isLoadingPage = !isOffline && (isLoadingFeed || isLoadingOnyxValue(personalDetailsMetadata) || areWorkspaceCardFeedsLoading);
+ const isShowingLoadingState = isLoadingPage || isLoadingFeed;
- const isLoadingPage = !isOffline && (isLoadingFeed || isLoadingOnyxValue(personalDetailsMetadata));
- const showCards = !isInitiallyLoadingFeeds && !isFeedPending && !isNoFeed && !isLoadingFeed;
- const showTableControls = showCards && !!selectedFeed && !isLoadingCards;
- const showTableHeaderButtons = (showTableControls || isLoadingPage || isFeedPending) && !!feedName;
+ const showCards = !isInitiallyLoadingFeeds && !isFeedPending && !isNoFeed && !isLoadingFeed && !hasFeedErrors;
+ const showTableControls = showCards && !!selectedFeed && !isLoadingCards && !hasFeedErrors;
+ const showTableHeaderButtons = (showTableControls || isLoadingPage || isFeedPending || feedErrorKey === CONST.COMPANY_CARDS.FEED_LOAD_ERROR) && !!feedName;
const isGB = countryByIp === CONST.COUNTRY.GB;
const shouldShowGBDisclaimer = isGB && (isNoFeed || hasNoAssignedCard);
@@ -272,6 +311,7 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace
isNarrowLayoutRef.current = true;
const activeSorting = tableRef.current?.getActiveSorting();
+ // eslint-disable-next-line react-hooks/set-state-in-effect
setActiveSortingInWideLayout(activeSorting);
tableRef.current?.updateSorting({columnKey: 'member', order: 'asc'});
return;
@@ -285,6 +325,11 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace
tableRef.current?.updateSorting(activeSortingInWideLayout);
}, [activeSortingInWideLayout, shouldUseNarrowTableLayout]);
+ const illustrations = useMemoizedLazyIllustrations(['BrokenMagnifyingGlass']);
+ const bottomSafeAreaPaddingStyle = useBottomSafeSafeAreaPaddingStyle({
+ addBottomSafeAreaPadding: true,
+ });
+
const headerButtonsComponent = showTableHeaderButtons ? (
{shouldRenderHeaderAsChild && headerButtonsComponent}
- {(isLoadingPage || isFeedPending || isNoFeed) && (
+ {(isLoadingPage || isFeedPending || isNoFeed) && !feedErrorKey && (
{isLoadingPage && }
@@ -334,6 +379,27 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace
)}
+ {!!feedErrorKey && !isShowingLoadingState && (
+
+
+
+
+
+
+ )}
+
{showCards && (
<>
{!shouldUseNarrowTableLayout && !isLoadingFeed && }
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 044d9b93c1045..745a495761060 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -2553,12 +2553,18 @@ const staticStyles = (theme: ThemeColors) =>
alignSelf: 'center',
},
- blockingErrorViewContainer: {
+ searchBlockingErrorViewContainer: {
paddingBottom: variables.contentHeaderHeight,
maxWidth: 475,
alignSelf: 'center',
},
+ companyCardsBlockingErrorViewContainer: {
+ maxWidth: 475,
+ alignSelf: 'center',
+ flex: undefined,
+ },
+
forcedBlockingViewContainer: {
...positioning.pFixed,
top: 0,
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 4c509891322d2..276a7ff7981ca 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -324,6 +324,9 @@ export default {
errorPageIconWidth: 116,
errorPageIconHeight: 168,
+ companyCardsPageNotFoundIconWidth: 97,
+ companyCardsPageNotFoundIconHeight: 140,
+
h20: 20,
h28: 28,
h36: 36,
diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts
index b566c1540010a..0b5c580a90d7b 100644
--- a/src/types/onyx/CardFeeds.ts
+++ b/src/types/onyx/CardFeeds.ts
@@ -154,6 +154,25 @@ type DomainSettings = {
};
};
+/** Card feeds status */
+type CardFeedsStatus = {
+ /** Whether we are loading the data via the API */
+ isLoading?: boolean;
+
+ /** Collection of errors coming from BE */
+ errors?: OnyxCommon.Errors;
+};
+
+/**
+ * Collection of card feeds status by domain ID
+ */
+type CardFeedsStatusByDomainID = Record;
+
+/**
+ * Collection of card feeds status by domain ID
+ */
+type WorkspaceCardFeedsStatus = Record;
+
/** Card feeds model, including domain settings */
type CardFeeds = {
/** Feed settings */
@@ -167,16 +186,17 @@ type CardFeeds = {
/** Account details */
oAuthAccountDetails?: Partial>;
+ /** Collection of card feeds status by domain ID */
+ cardFeedsStatus?: WorkspaceCardFeedsStatus;
+
/** Email address of the technical contact for the domain */
technicalContactEmail?: string;
/** Whether to use the technical contact's billing card */
useTechnicalContactBillingCard?: boolean;
};
-
- /** Whether we are loading the data via the API */
- isLoading?: boolean;
-} & DomainSettings;
+} & CardFeedsStatus &
+ DomainSettings;
/** Data required to be sent to add a new card */
type AddNewCardFeedData = {
@@ -249,6 +269,9 @@ type CombinedCardFeed = CustomCardFeedData &
/** Feed name */
feed: CompanyCardFeedWithNumber;
+
+ /** Card feed status */
+ status?: CardFeedsStatus;
};
/** Card feeds combined by domain ID into one object */
@@ -264,6 +287,9 @@ export type {
DirectCardFeedData,
CardFeedProvider,
CardFeedData,
+ CardFeedsStatus,
+ CardFeedsStatusByDomainID,
+ WorkspaceCardFeedsStatus,
CompanyFeeds,
CompanyCardFeedWithDomainID,
CustomCardFeedData,
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 698337f0a3f31..2e254bcd4ab74 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -19,7 +19,18 @@ import type Card from './Card';
import type {CardList, FailedCompanyCardAssignment, FailedCompanyCardAssignments, IssueNewCard, ProvisioningCardData, WorkspaceCardsList} from './Card';
import type CardContinuousReconciliation from './CardContinuousReconciliation';
import type CardFeeds from './CardFeeds';
-import type {AddNewCompanyCardFeed, CombinedCardFeed, CombinedCardFeeds, CompanyCardFeed, CompanyCardFeedWithDomainID, DomainSettings, FundID} from './CardFeeds';
+import type {
+ AddNewCompanyCardFeed,
+ CardFeedsStatus,
+ CardFeedsStatusByDomainID,
+ CombinedCardFeed,
+ CombinedCardFeeds,
+ CompanyCardFeed,
+ CompanyCardFeedWithDomainID,
+ DomainSettings,
+ FundID,
+ WorkspaceCardFeedsStatus,
+} from './CardFeeds';
import type CardOnWaitlist from './CardOnWaitlist';
import type {CapturedLogs, Log} from './Console';
import type {CorpayFields, CorpayFormField} from './CorpayFields';
@@ -286,6 +297,9 @@ export type {
CancellationDetails,
ApprovalWorkflowOnyx,
CardFeeds,
+ CardFeedsStatus,
+ CardFeedsStatusByDomainID,
+ WorkspaceCardFeedsStatus,
DomainSettings,
SaveSearch,
RecentSearchItem,