Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[No QA] [Direct Feeds] Broken connection - feed level and Update card sync #50587

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2518,6 +2518,7 @@ const CONST = {
CARD_TITLE_INPUT_LIMIT: 255,
},
COMPANY_CARDS: {
CONNECTION_ERROR: 'connectionError',
STEP: {
CARD_TYPE: 'CardType',
CARD_INSTRUCTIONS: 'CardInstructions',
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 @@ -3001,6 +3001,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 @@ -3038,6 +3038,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, `,
mountiny marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -4536,6 +4536,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 @@ -4569,6 +4580,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
Loading