diff --git a/assets/images/emptystate__expensifycard.svg b/assets/images/emptystate__expensifycard.svg new file mode 100644 index 000000000000..c5699c059e69 --- /dev/null +++ b/assets/images/emptystate__expensifycard.svg @@ -0,0 +1,909 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index a8ad9d0f3154..41d1a42931c6 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -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); @@ -76,7 +87,7 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b /> - + {HeaderComponent} {title} diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts index 326b25542f42..96a60fa98513 100644 --- a/src/components/EmptyStateComponent/types.ts +++ b/src/components/EmptyStateComponent/types.ts @@ -19,6 +19,7 @@ type SharedProps = { headerStyles?: StyleProp; headerMediaType: T; headerContentStyles?: StyleProp; + emptyStateContentStyles?: StyleProp; }; type MediaType = SharedProps & { diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 6499e8eceb6e..ccbf4b3a5da9 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -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'; @@ -115,6 +116,7 @@ export { ConciergeExclamation, CreditCardsBlue, EmailAddress, + EmptyCardState, EmptyStateExpenses, FolderOpen, HandCard, diff --git a/src/components/Skeletons/CardRowSkeleton.tsx b/src/components/Skeletons/CardRowSkeleton.tsx new file mode 100644 index 000000000000..82c382048415 --- /dev/null +++ b/src/components/Skeletons/CardRowSkeleton.tsx @@ -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 ( + ( + <> + + + + + + {!isSmallScreenWidth && ( + <> + + + + + )} + + )} + /> + ); +} + +CardRowSkeleton.displayName = 'CardRowSkeleton'; + +export default CardRowSkeleton; diff --git a/src/languages/en.ts b/src/languages/en.ts index 1f7f91cb8472..9bfe8e0faf38 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -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', @@ -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?', diff --git a/src/languages/es.ts b/src/languages/es.ts index dbef29504a34..a3bc94e81352 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -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', @@ -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', diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index b775be2fd031..b128e8f3045d 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -379,9 +379,9 @@ export { reportVirtualExpensifyCardFraud, revealVirtualCardDetails, updateSettlementFrequency, - updateSettlementAccount, setIssueNewCardStepAndData, clearIssueNewCardFlow, updateExpensifyCardLimit, + updateSettlementAccount, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/card/issueNew/AssigneeStep.tsx b/src/pages/workspace/card/issueNew/AssigneeStep.tsx index 042e93ae2fae..3190c4ba295c 100644 --- a/src/pages/workspace/card/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/card/issueNew/AssigneeStep.tsx @@ -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; @@ -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(); }; diff --git a/src/pages/workspace/expensifyCard/EmptyCardView.tsx b/src/pages/workspace/expensifyCard/EmptyCardView.tsx new file mode 100644 index 000000000000..9e82454f0509 --- /dev/null +++ b/src/pages/workspace/expensifyCard/EmptyCardView.tsx @@ -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 ( + + + + + {translate('workspace.expensifyCard.disclaimer')} + + ); +} + +EmptyCardView.displayName = 'EmptyCardView'; + +export default EmptyCardView; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx index e1ca8974a0ee..470b2c683e70 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx @@ -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'; @@ -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)); }; @@ -49,6 +51,8 @@ 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}); @@ -56,7 +60,7 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA handleSelectBankAccount(bankAccountID)} icon={icon} iconHeight={iconSize} iconWidth={iconSize} diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index b8ab57212564..742a5c2a290e 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -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'; @@ -160,14 +162,16 @@ function WorkspaceExpensifyCardListPage({route}: WorkspaceExpensifyCardListPageP > {!shouldUseNarrowLayout && getHeaderButtons()} - {shouldUseNarrowLayout && {getHeaderButtons()}} - - + {!isEmptyObject(cardsList) ? ( + + ) : ( + + )} ); } diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx index 25be3775964c..b6df6b662eb9 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx @@ -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; -// 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 = {}; - 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 ( {/* After BE will be implemented we will probably want to have ActivityIndicator during fetch for cardsList */} - {isEmptyObject(cardsList) && } - {!isEmptyObject(cardsList) && } + {paymentBankAccountID ? : } ); } diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx index ac6c03dd18b3..ee0ab87af72e 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx @@ -82,7 +82,6 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif illustration={Illustrations.ExpensifyCardIllustration} illustrationStyle={styles.expensifyCardIllustrationContainer} titleStyles={styles.textHeadlineH1} - contentPaddingOnLargeScreens={styles.p5} />