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] Empty state modal #45690

Merged
merged 13 commits into from
Jul 22, 2024
909 changes: 909 additions & 0 deletions assets/images/emptystate__expensifycard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 13 additions & 2 deletions src/components/EmptyStateComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,18 @@ import type {EmptyStateComponentProps, VideoLoadedEventType} from './types';

const VIDEO_ASPECT_RATIO = 400 / 225;

function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, title, subtitle, headerStyles, headerContentStyles}: EmptyStateComponentProps) {
function EmptyStateComponent({
SkeletonComponent,
headerMediaType,
headerMedia,
buttonText,
buttonAction,
title,
subtitle,
headerStyles,
headerContentStyles,
emptyStateContentStyles,
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
Expand Down Expand Up @@ -76,7 +87,7 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b
/>
</View>
<View style={styles.emptyStateForeground(isSmallScreenWidth)}>
<View style={styles.emptyStateContent}>
<View style={[styles.emptyStateContent, emptyStateContentStyles]}>
<View style={[styles.emptyStateHeader(headerMediaType === CONST.EMPTY_STATE_MEDIA.ILLUSTRATION), headerStyles]}>{HeaderComponent}</View>
<View style={styles.p8}>
<Text style={[styles.textAlignCenter, styles.textHeadlineH1, styles.mb2]}>{title}</Text>
Expand Down
1 change: 1 addition & 0 deletions src/components/EmptyStateComponent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type SharedProps<T> = {
headerStyles?: StyleProp<ViewStyle>;
headerMediaType: T;
headerContentStyles?: StyleProp<ViewStyle & ImageStyle>;
emptyStateContentStyles?: StyleProp<ViewStyle>;
};

type MediaType<HeaderMedia, T extends MediaTypes> = SharedProps<T> & {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment to add reviewer

import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg';
import LaptopwithSecondScreenandHourglass from '@assets/images/LaptopwithSecondScreenandHourglass.svg';
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
Expand Down Expand Up @@ -115,6 +116,7 @@ export {
ConciergeExclamation,
CreditCardsBlue,
EmailAddress,
EmptyCardState,
EmptyStateExpenses,
FolderOpen,
HandCard,
Expand Down
81 changes: 81 additions & 0 deletions src/components/Skeletons/CardRowSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from 'react';
import {Circle, Rect} from 'react-native-svg';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import variables from '@styles/variables';
import ItemListSkeletonView from './ItemListSkeletonView';

type CardRowSkeletonProps = {
shouldAnimate?: boolean;
fixedNumItems?: number;
gradientOpacityEnabled?: boolean;
};

const barHeight = 7;
const longBarWidth = 120;
const shortBarWidth = 60;
const leftPaneWidth = variables.sideBarWidth;
const gapWidth = 12;
const rightSideElementWidth = 50;
const centralPanePadding = 50;
const rightButtonWidth = 20;

function CardRowSkeleton({shouldAnimate = true, fixedNumItems, gradientOpacityEnabled = false}: CardRowSkeletonProps) {
const styles = useThemeStyles();
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();

return (
<ItemListSkeletonView
shouldAnimate={shouldAnimate}
fixedNumItems={fixedNumItems}
gradientOpacityEnabled={gradientOpacityEnabled}
itemViewStyle={[styles.highlightBG, styles.mb3, styles.br3, styles.mr3, styles.ml3]}
renderSkeletonItem={() => (
<>
<Circle
cx={36}
cy={32}
r={20}
/>
<Rect
x={66}
y={22}
width={longBarWidth}
height={barHeight}
/>

<Rect
x={66}
y={36}
width={shortBarWidth}
height={barHeight}
/>

{!isSmallScreenWidth && (
<>
<Rect
// We have to calculate this value to make sure the element is aligned to the button on the right side.
x={windowWidth - leftPaneWidth - rightButtonWidth - gapWidth - centralPanePadding - gapWidth - rightSideElementWidth}
y={28}
width={20}
height={barHeight}
/>

<Rect
// We have to calculate this value to make sure the element is aligned to the right border.
x={windowWidth - leftPaneWidth - rightSideElementWidth - gapWidth - centralPanePadding}
y={28}
width={50}
height={barHeight}
/>
</>
)}
</>
)}
/>
);
}

CardRowSkeleton.displayName = 'CardRowSkeleton';

export default CardRowSkeleton;
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2653,6 +2653,10 @@ export default {
collect: 'Collect',
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
getStartedIssuing: 'Get started by issuing your first virtual or physical card.',
disclaimer:
'The Expensify Visa® Commercial Card is issued by The Bancorp Bank, N.A., Member FDIC, pursuant to a license from Visa U.S.A. Inc. and may not be used at all merchants that accept Visa cards. Apple® and the Apple logo® are trademarks of Apple Inc., registered in the U.S. and other countries. App Store is a service mark of Apple Inc. Google Play and the Google Play logo are trademarks of Google LLC.',
issueCard: 'Issue card',
name: 'Name',
lastFour: 'Last 4',
Expand Down Expand Up @@ -2955,6 +2959,8 @@ export default {
benefit4: 'Customizable limits and spend controls',
addWorkEmail: 'Add work email address',
checkingDomain: "Hang tight! We're still working on enabling your Expensify Cards. Check back here in a few minutes.",
issueAndManageCards: 'Issue and manage your Expensify Cards',
getStartedIssuing: 'Get started by issuing your first virtual or physical card.',
issueCard: 'Issue card',
issueNewCard: {
whoNeedsCard: 'Who needs a card?',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,10 @@ export default {
collect: 'Recolectar',
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
getStartedIssuing: 'Empieza emitiendo tu primera tarjeta virtual o física.',
disclaimer:
'La tarjeta comercial Expensify Visa® es emitida por The Bancorp Bank, N.A., miembro de la FDIC, en virtud de una licencia de Visa U.S.A. Inc. y no puede utilizarse en todos los comercios que aceptan tarjetas Visa. Apple® y el logotipo de Apple® son marcas comerciales de Apple Inc. registradas en EE.UU. y otros países. App Store es una marca de servicio de Apple Inc. Google Play y el logotipo de Google Play son marcas comerciales de Google LLC.',
issueCard: 'Emitir tarjeta',
name: 'Nombre',
lastFour: '4 últimos',
Expand Down Expand Up @@ -3210,6 +3214,8 @@ export default {
addWorkEmail: 'Añadir correo electrónico de trabajo',
checkingDomain: '¡Un momento! Estamos todavía trabajando para habilitar tu Tarjeta Expensify. Vuelve aquí en unos minutos.',
issueCard: 'Emitir tarjeta',
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
getStartedIssuing: 'Empieza emitiendo tu primera tarjeta virtual o física.',
issueNewCard: {
whoNeedsCard: '¿Quién necesita una tarjeta?',
findMember: 'Buscar miembro',
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,9 +379,9 @@ export {
reportVirtualExpensifyCardFraud,
revealVirtualCardDetails,
updateSettlementFrequency,
updateSettlementAccount,
setIssueNewCardStepAndData,
clearIssueNewCardFlow,
updateExpensifyCardLimit,
updateSettlementAccount,
};
export type {ReplacementReason};
3 changes: 2 additions & 1 deletion src/pages/workspace/card/issueNew/AssigneeStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Navigation from '@navigation/Navigation';
import * as Card from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';

const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8;
Expand Down Expand Up @@ -56,7 +57,7 @@ function AssigneeStep({policy}: AssigneeStepProps) {
Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false});
return;
}
Navigation.goBack();
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id ?? '-1'));
Card.clearIssueNewCardFlow();
};

Expand Down
44 changes: 44 additions & 0 deletions src/pages/workspace/expensifyCard/EmptyCardView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import {View} from 'react-native';
import EmptyStateComponent from '@components/EmptyStateComponent';
import * as Illustrations from '@components/Icon/Illustrations';
import ScrollView from '@components/ScrollView';
import CardRowSkeleton from '@components/Skeletons/CardRowSkeleton';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import CONST from '@src/CONST';

const HEADER_HEIGHT = 80;
const BUTTON_HEIGHT = 40;
const BUTTON_MARGIN = 12;

function EmptyCardView() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const {windowHeight, isSmallScreenWidth} = useWindowDimensions();

const headerHeight = isSmallScreenWidth ? HEADER_HEIGHT + BUTTON_HEIGHT + BUTTON_MARGIN : HEADER_HEIGHT;

return (
<ScrollView>
<View style={{height: windowHeight - headerHeight}}>
<EmptyStateComponent
SkeletonComponent={CardRowSkeleton}
headerMediaType={CONST.EMPTY_STATE_MEDIA.ILLUSTRATION}
headerMedia={Illustrations.EmptyCardState}
headerStyles={[{overflow: 'hidden'}, isSmallScreenWidth && {maxHeight: 300}]}
title={translate('workspace.expensifyCard.issueAndManageCards')}
subtitle={translate('workspace.expensifyCard.getStartedIssuing')}
emptyStateContentStyles={isSmallScreenWidth ? {top: -BUTTON_HEIGHT} : undefined}
/>
</View>
<Text style={[styles.textMicroSupporting, styles.m5]}>{translate('workspace.expensifyCard.disclaimer')}</Text>
</ScrollView>
);
}

EmptyCardView.displayName = 'EmptyCardView';

export default EmptyCardView;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {getLastFourDigits} from '@libs/BankAccountUtils';
import * as CardUtils from '@libs/CardUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import * as Card from '@userActions/Card';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand All @@ -34,7 +35,8 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('new', policyID, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID)));
};

const handleSelectBankAccount = () => {
const handleSelectBankAccount = (value: number) => {
Card.updateSettlementAccount(policyID, value);
Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.getRoute(policyID));
};

Expand All @@ -49,14 +51,16 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA
return eligibleBankAccounts.map((bankAccount) => {
const bankName = (bankAccount.accountData?.addressName ?? '') as BankName;
const bankAccountNumber = bankAccount.accountData?.accountNumber ?? '';
// TODO: change 1 to 0 - applied for testing purposes, as sometimes accountData lacks fundID
const bankAccountID = bankAccount.accountData?.fundID ?? 1;

const {icon, iconSize, iconStyles} = getBankIcon({bankName, styles});

return (
<MenuItem
title={bankName}
description={`${translate('workspace.expensifyCard.accountEndingIn')} ${getLastFourDigits(bankAccountNumber)}`}
onPress={handleSelectBankAccount}
onPress={() => handleSelectBankAccount(bankAccountID)}
icon={icon}
iconHeight={iconSize}
iconWidth={iconSize}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Card, WorkspaceCardsList} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import EmptyCardView from './EmptyCardView';
import WorkspaceCardListHeader from './WorkspaceCardListHeader';
import WorkspaceCardListRow from './WorkspaceCardListRow';

Expand Down Expand Up @@ -160,14 +162,16 @@ function WorkspaceExpensifyCardListPage({route}: WorkspaceExpensifyCardListPageP
>
{!shouldUseNarrowLayout && getHeaderButtons()}
</HeaderWithBackButton>

{shouldUseNarrowLayout && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}

<FlatList
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={WorkspaceCardListHeader}
/>
{!isEmptyObject(cardsList) ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@koko57 Please help to explain this line

As I understand, we display EmptyCardView if cardList í empty

Copy link
Contributor Author

@koko57 koko57 Aug 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DylanDylann yes, I did it probably bc I didn't want to remove mockedCard list in this PR. I'll tell Vicky (she's integrating BE with the cardList), to correct this condition

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<EmptyCardView />
) : (
<FlatList
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={WorkspaceCardListHeader}
/>
)}
</ScreenWrapper>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type {WorkspaceCardsList} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import WorkspaceExpensifyCardListPage from './WorkspaceExpensifyCardListPage';
import WorkspaceExpensifyCardPageEmptyState from './WorkspaceExpensifyCardPageEmptyState';

type WorkspaceExpensifyCardPageProps = StackScreenProps<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.EXPENSIFY_CARD>;

// TODO: remove when Onyx data is available, and pass the data to 'WorkspaceExpensifyCardListPage' so that we will not make the same 'Onyx' call twice
const cardsList: OnyxEntry<WorkspaceCardsList> = {};

function WorkspaceExpensifyCardPage({route}: WorkspaceExpensifyCardPageProps) {
// const policyID = route.params.policyID ?? '-1';
// const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`);
const policyID = route.params.policyID ?? '-1';
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`);

const paymentBankAccountID = cardSettings?.paymentBankAccountID ?? -1;

return (
<AccessOrNotFoundWrapper
Expand All @@ -26,8 +24,7 @@ function WorkspaceExpensifyCardPage({route}: WorkspaceExpensifyCardPageProps) {
featureName={CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED}
>
{/* After BE will be implemented we will probably want to have ActivityIndicator during fetch for cardsList */}
{isEmptyObject(cardsList) && <WorkspaceExpensifyCardPageEmptyState route={route} />}
{!isEmptyObject(cardsList) && <WorkspaceExpensifyCardListPage route={route} />}
{paymentBankAccountID ? <WorkspaceExpensifyCardListPage route={route} /> : <WorkspaceExpensifyCardPageEmptyState route={route} />}
</AccessOrNotFoundWrapper>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif
illustration={Illustrations.ExpensifyCardIllustration}
illustrationStyle={styles.expensifyCardIllustrationContainer}
titleStyles={styles.textHeadlineH1}
contentPaddingOnLargeScreens={styles.p5}
/>
</View>
</WorkspacePageWithSections>
Expand Down
Loading