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 && ( + + + +