Skip to content

Commit

Permalink
Merge pull request #45690 from koko57/feat/44306-empty-state-modal
Browse files Browse the repository at this point in the history
[No QA] Empty state modal
  • Loading branch information
mountiny authored Jul 22, 2024
2 parents 854ea80 + 8c91902 commit 146b847
Show file tree
Hide file tree
Showing 14 changed files with 1,089 additions and 24 deletions.
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';
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 @@ -2645,6 +2645,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 @@ -2947,6 +2951,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 @@ -2694,6 +2694,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 @@ -3204,6 +3208,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) ? (
<EmptyCardView />
) : (
<FlatList
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={WorkspaceCardListHeader}
/>
)}
</ScreenWrapper>
);
}
Expand Down
17 changes: 7 additions & 10 deletions src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx
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

0 comments on commit 146b847

Please sign in to comment.