Skip to content

Commit

Permalink
Merge pull request #50587 from VickyStash/feature/50453-feed-broken-c…
Browse files Browse the repository at this point in the history
…onnection

[No QA] [Direct Feeds] Broken connection - feed level and Update card sync
  • Loading branch information
mountiny authored Oct 11, 2024
2 parents 9490328 + f835a60 commit f83af86
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 70 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2548,6 +2548,7 @@ const CONST = {
CARD_TITLE_INPUT_LIMIT: 255,
},
COMPANY_CARDS: {
CONNECTION_ERROR: 'connectionError',
STEP: {
SELECT_BANK: 'SelectBank',
CARD_TYPE: 'CardType',
Expand Down
86 changes: 56 additions & 30 deletions src/components/DotIndicatorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {isReceiptError} from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
import * as Localize from '@libs/Localize';
import CONST from '@src/CONST';
import type {ReceiptError} from '@src/types/onyx/Transaction';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
Expand Down Expand Up @@ -53,6 +54,60 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica

const isErrorMessage = type === 'error';

const renderMessage = (message: string | ReceiptError, index: number) => {
if (isReceiptError(message)) {
return (
<Text
key={index}
style={styles.offlineFeedback.text}
>
<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('iou.error.receiptFailureMessage')}</Text>
<TextLink
style={[StyleUtils.getDotIndicatorTextStyles(), styles.link]}
onPress={() => {
fileDownload(message.source, message.filename);
}}
>
{Localize.translateLocal('iou.error.saveFileMessage')}
</TextLink>

<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('iou.error.loseFileMessage')}</Text>
</Text>
);
}

if (message === CONST.COMPANY_CARDS.CONNECTION_ERROR) {
return (
<Text
key={index}
style={styles.offlineFeedback.text}
>
<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('workspace.companyCards.brokenConnectionErrorFirstPart')}</Text>
<TextLink
style={[StyleUtils.getDotIndicatorTextStyles(), styles.link]}
onPress={() => {
// TODO: re-navigate the user to the bank’s website to re-authenticate https://github.com/Expensify/App/issues/50448
}}
>
{Localize.translateLocal('workspace.companyCards.brokenConnectionErrorLink')}
</TextLink>

<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('workspace.companyCards.brokenConnectionErrorSecondPart')}</Text>
</Text>
);
}

return (
<Text
// eslint-disable-next-line react/no-array-index-key
key={index}
style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage), textStyles]}
>
{message}
</Text>
);
};

return (
<View style={[styles.dotIndicatorMessage, style]}>
<View style={styles.offlineFeedback.errorDot}>
Expand All @@ -61,36 +116,7 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica
fill={isErrorMessage ? theme.danger : theme.success}
/>
</View>
<View style={styles.offlineFeedback.textContainer}>
{uniqueMessages.map((message, i) =>
isReceiptError(message) ? (
<Text
key={i}
style={styles.offlineFeedback.text}
>
<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('iou.error.receiptFailureMessage')}</Text>
<TextLink
style={[StyleUtils.getDotIndicatorTextStyles(), styles.link]}
onPress={() => {
fileDownload(message.source, message.filename);
}}
>
{Localize.translateLocal('iou.error.saveFileMessage')}
</TextLink>

<Text style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage)]}>{Localize.translateLocal('iou.error.loseFileMessage')}</Text>
</Text>
) : (
<Text
// eslint-disable-next-line react/no-array-index-key
key={i}
style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage), textStyles]}
>
{message}
</Text>
),
)}
</View>
<View style={styles.offlineFeedback.textContainer}>{uniqueMessages.map(renderMessage)}</View>
</View>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/SelectionList/BaseListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function BaseListItem<TItem extends ListItem>({
</View>
</View>
)}
{!item.isSelected && !!item.brickRoadIndicator && shouldDisplayRBR && (
{(!item.isSelected || item.canShowSeveralIndicators) && !!item.brickRoadIndicator && shouldDisplayRBR && (
<View style={[styles.alignItemsCenter, styles.justifyContentCenter]}>
<Icon
src={Expensicons.DotIndicator}
Expand Down
3 changes: 3 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ type ListItem = {
/** Whether this option is selected */
isSelected?: boolean;

/** Whether the option can show both selected and error indicators */
canShowSeveralIndicators?: boolean;

/** Whether the checkbox should be disabled */
isDisabledCheckbox?: boolean;

Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3088,6 +3088,9 @@ const translations = {
card: 'Card',
startTransactionDate: 'Start transaction date',
cardName: 'Card name',
brokenConnectionErrorFirstPart: `Card feed connection is broken. Please `,
brokenConnectionErrorLink: 'log into your bank ',
brokenConnectionErrorSecondPart: 'so we can establish the connection again.',
assignedYouCard: ({assigner}: AssignedYouCardParams) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
chooseCardFeed: 'Choose card feed',
},
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3127,6 +3127,9 @@ const translations = {
card: 'Tarjeta',
startTransactionDate: 'Fecha de inicio de transacciones',
cardName: 'Nombre de la tarjeta',
brokenConnectionErrorFirstPart: `La conexión de la fuente de tarjetas está rota. Por favor, `,
brokenConnectionErrorLink: 'inicia sesión en tu banco ',
brokenConnectionErrorSecondPart: 'para que podamos restablecer la conexión.',
assignedYouCard: ({assigner}: AssignedYouCardParams) => ${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
chooseCardFeed: 'Elige feed de tarjetas',
},
Expand Down
6 changes: 6 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, TaxRate} from '@src/types/onyx';
import type {CardFeedData} from '@src/types/onyx/CardFeeds';
import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon';
import type {
ConnectionLastSync,
Expand Down Expand Up @@ -1040,6 +1041,10 @@ function getWorkflowApprovalsUnavailable(policy: OnyxEntry<Policy>) {
return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL || !!policy?.errorFields?.approvalMode;
}

function hasPolicyFeedsError(feeds: Record<string, CardFeedData>, feedToSkip?: string): boolean {
return Object.entries(feeds).filter(([feedName, feedData]) => feedName !== feedToSkip && !!feedData.errors).length > 0;
}

export {
canEditTaxRate,
extractPolicyIDFromPath,
Expand Down Expand Up @@ -1068,6 +1073,7 @@ export {
goBackFromInvalidPolicy,
hasAccountingConnections,
hasSyncError,
hasPolicyFeedsError,
hasCustomUnitsError,
hasEmployeeListError,
hasIntegrationAutoSync,
Expand Down
22 changes: 22 additions & 0 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4542,6 +4542,17 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
companyCards: {
[bankName]: {
errors: null,
},
},
},
},
];

const finallyData: OnyxUpdate[] = [
Expand Down Expand Up @@ -4575,6 +4586,17 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`,
value: {
companyCards: {
[bankName]: {
errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR},
},
},
},
},
];

const parameters = {
Expand Down
3 changes: 3 additions & 0 deletions src/pages/workspace/WorkspaceInitialPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ function dismissError(policyID: string, pendingAction: PendingAction | undefined
function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: WorkspaceInitialPageProps) {
const styles = useThemeStyles();
const policy = policyDraft?.id ? policyDraft : policyProp;
const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1');
const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false);
const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors));
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`);
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID ?? '-1'}`);
const hasSyncError = PolicyUtils.hasSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy));
Expand Down Expand Up @@ -215,6 +217,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
icon: Expensicons.CreditCard,
action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))),
routeName: SCREENS.WORKSPACE.COMPANY_CARDS,
brickRoadIndicator: PolicyUtils.hasPolicyFeedsError(cardFeeds?.companyCards ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const mockedData: CardFeeds = {
asrEnabled: true,
forceReimbursable: 'force_no',
liabilityType: 'corporate',
errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR},
preferredPolicy: '',
reportTitleFormat: '{report:card}{report:bank}{report:submit:from}{report:total}{report:enddate:MMMM}',
statementPeriodEndDay: 'LAST_DAY_OF_MONTH',
Expand All @@ -58,13 +59,15 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS

const cardFeeds = mockedData;
const defaultFeed = Object.keys(cardFeeds?.companyCards ?? {}).at(0);
const selectedFeed = lastSelectedFeed ?? defaultFeed;
const selectedFeed = lastSelectedFeed ?? defaultFeed ?? '';

const feeds: CardFeedListItem[] = Object.entries(cardFeeds?.companyCardNicknames ?? {}).map(([key, value]) => ({
value: key,
text: value,
keyForList: key,
isSelected: key === selectedFeed,
brickRoadIndicator: CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR,
canShowSeveralIndicators: !!cardFeeds?.companyCards?.[selectedFeed]?.errors,
leftElement: (
<Icon
src={CardUtils.getCardFeedIcon(key)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import EmptyStateComponent from '@components/EmptyStateComponent';
import * as Illustrations from '@components/Icon/Illustrations';
import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -14,10 +15,10 @@ function WorkspaceCompanyCardsFeedPendingPage() {
const styles = useThemeStyles();

const subtitle = (
<>
<Text>
{translate('workspace.moreFeatures.companyCards.pendingFeedDescription')}
<TextLink onPress={() => ReportInstance.navigateToConciergeChat()}> {CONST?.CONCIERGE_CHAT_NAME}</TextLink>
</>
</Text>
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import Button from '@components/Button';
import CaretWrapper from '@components/CaretWrapper';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
Expand All @@ -28,49 +30,64 @@ type WorkspaceCompanyCardsListHeaderButtonsProps = {
function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: WorkspaceCompanyCardsListHeaderButtonsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const theme = useTheme();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout;

return (
<View style={[styles.w100, styles.ph5, !shouldChangeLayout ? [styles.pv2, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween] : styles.pb2]}>
<PressableWithFeedback
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, styles.ml4, shouldUseNarrowLayout && styles.mb3]}
accessibilityLabel={cardFeeds?.companyCardNicknames?.[selectedFeed] ?? ''}
>
<Icon
src={CardUtils.getCardFeedIcon(selectedFeed)}
width={variables.iconSizeExtraLarge}
height={variables.iconSizeExtraLarge}
/>
<View>
<CaretWrapper>
<Text style={styles.textStrong}>{cardFeeds?.companyCardNicknames?.[selectedFeed]}</Text>
</CaretWrapper>
<Text style={styles.textLabelSupporting}>{translate('workspace.companyCards.customFeed')}</Text>
</View>
</PressableWithFeedback>
<OfflineWithFeedback
errors={cardFeeds?.companyCards?.[selectedFeed]?.errors}
canDismissError={false}
errorRowStyles={styles.ph5}
>
<View style={[styles.w100, styles.ph5, !shouldChangeLayout ? [styles.pv2, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween] : styles.pb2]}>
<PressableWithFeedback
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldChangeLayout && styles.mb3]}
accessibilityLabel={cardFeeds?.companyCardNicknames?.[selectedFeed] ?? ''}
>
<Icon
src={CardUtils.getCardFeedIcon(selectedFeed)}
width={variables.iconSizeExtraLarge}
height={variables.iconSizeExtraLarge}
/>
<View>
<View style={[styles.flexRow, styles.gap1]}>
<CaretWrapper>
<Text style={styles.textStrong}>{cardFeeds?.companyCardNicknames?.[selectedFeed]}</Text>
</CaretWrapper>
{PolicyUtils.hasPolicyFeedsError(cardFeeds?.companyCards ?? {}, selectedFeed) && (
<Icon
src={Expensicons.DotIndicator}
fill={theme.danger}
/>
)}
</View>
<Text style={styles.textLabelSupporting}>{translate('workspace.companyCards.customFeed')}</Text>
</View>
</PressableWithFeedback>

<View style={[styles.flexRow, styles.gap2]}>
<Button
success
isDisabled={cardFeeds?.companyCards?.[selectedFeed].pending ?? false}
// TODO: navigate to Assign card flow when it's implemented
onPress={() => {}}
icon={Expensicons.Plus}
text={translate('workspace.companyCards.assignCard')}
style={shouldChangeLayout && styles.flex1}
/>
<Button
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID))}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={shouldChangeLayout && styles.flex1}
/>
<View style={[styles.flexRow, styles.gap2]}>
<Button
success
isDisabled={!!cardFeeds?.companyCards?.[selectedFeed].pending || !!cardFeeds?.companyCards?.[selectedFeed].errors}
// TODO: navigate to Assign card flow when it's implemented
onPress={() => {}}
icon={Expensicons.Plus}
text={translate('workspace.companyCards.assignCard')}
style={shouldChangeLayout && styles.flex1}
/>
<Button
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID))}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={shouldChangeLayout && styles.flex1}
/>
</View>
</View>
</View>
</OfflineWithFeedback>
);
}

Expand Down
Loading

0 comments on commit f83af86

Please sign in to comment.