diff --git a/assets/images/simple-illustrations/simple-illustration__virtualcard.svg b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg new file mode 100644 index 000000000000..2c1f538102a2 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__virtualcard.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 13d44ee883be..48870c74a699 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2130,6 +2130,10 @@ const CONST = { CARD_NAME: 'CardName', CONFIRMATION: 'Confirmation', }, + CARD_TYPE: { + PHYSICAL: 'physical', + VIRTUAL: 'virtual', + }, }, AVATAR_ROW_SIZE: { DEFAULT: 4, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5d6b5492d15c..2740b9e336b1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -556,8 +556,8 @@ const ONYXKEYS = { NEW_CHAT_NAME_FORM_DRAFT: 'newChatNameFormDraft', SUBSCRIPTION_SIZE_FORM: 'subscriptionSizeForm', SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', - ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCardForm', - ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardFormDraft', + ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', + ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', NETSUITE_TOKEN_INPUT_FORM: 'netsuiteTokenInputForm', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d548297cb854..1b9ab301c2a1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -815,13 +815,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, }, - // TODO: uncomment after development is done - // WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { - // route: 'settings/workspaces/:policyID/expensify-card/issues-new', - // getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, - // }, - // TODO: remove after development is done - this one is for testing purposes - WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: 'settings/workspaces/expensify-card/issue-new', + WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { + route: 'settings/workspaces/:policyID/expensify-card/issue-new', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index bd0824372799..5212f5b0edb7 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -92,6 +92,7 @@ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustrati import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; +import VirtualCard from '@assets/images/simple-illustrations/simple-illustration__virtualcard.svg'; import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg'; import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg'; import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg'; @@ -196,4 +197,5 @@ export { CheckmarkCircle, CreditCardEyes, LockClosedOrange, + VirtualCard, }; diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index be7ce9aca8b5..862b0ae5e928 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -97,11 +97,11 @@ function convertToFrontendAmountAsInteger(amountAsInt: number, currency: string * * @note we do not support any currencies with more than two decimal places. */ -function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD): string { +function convertToFrontendAmountAsString(amountAsInt: number | null | undefined, currency: string = CONST.CURRENCY.USD, withDecimals = true): string { if (amountAsInt === null || amountAsInt === undefined) { return ''; } - const decimals = getCurrencyDecimals(currency); + const decimals = withDecimals ? getCurrencyDecimals(currency) : 0; return convertToFrontendAmountAsInteger(amountAsInt, currency).toFixed(decimals); } diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 75b096fa4bbe..46bc66a3e3c0 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -129,7 +129,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS, SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE, ], - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [], + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 699cb9b704e1..65c4cf30bde9 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -438,7 +438,7 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACE_PROFILE_SHARE.route, }, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { - path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW, + path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route, }, [SCREENS.WORKSPACE.RATE_AND_UNIT]: { path: ROUTES.WORKSPACE_RATE_AND_UNIT.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a60316fb7768..0b5f5a9f7907 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -542,6 +542,9 @@ type SettingsNavigatorParamList = { policyID: string; taxID: string; }; + [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { + policyID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { @@ -993,7 +996,6 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.DISTANCE_RATES]: { policyID: string; }; - [SCREENS.WORKSPACE.ACCOUNTING.ROOT]: { policyID: string; }; @@ -1006,6 +1008,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR]: { policyID: string; }; + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: { + policyID: string; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f20d27ffdf22..330d9d6ef61d 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -223,6 +223,10 @@ type FilterOptionsConfig = Pick< 'sortByReportTypeInSearch' | 'canInviteUser' | 'betas' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' > & {preferChatroomsOverThreads?: boolean}; +type HasText = { + text?: string; +}; + /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public @@ -2559,6 +2563,10 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt }; } +function sortItemsAlphabetically(membersList: T[]): T[] { + return membersList.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase())); +} + export { getAvatarsForAccountIDs, isCurrentUser, @@ -2584,6 +2592,7 @@ export { getEnabledCategoriesCount, hasEnabledOptions, sortCategories, + sortItemsAlphabetically, sortTags, getCategoryOptionTree, hasEnabledTags, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index fc9a04e2507c..9c32e19f211a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -241,6 +241,7 @@ function getTagListName(policyTagList: OnyxEntry, orderWeight: nu return Object.values(policyTagList).find((tag) => tag.orderWeight === orderWeight)?.name ?? ''; } + /** * Gets all tag lists of a policy */ @@ -657,6 +658,13 @@ function getCurrentConnectionName(policy: Policy | undefined): string | undefine return connectionKey ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionKey] : undefined; } +/** + * Check if the policy member is deleted from the workspace + */ +function isDeletedPolicyEmployee(policyEmployee: PolicyEmployee, isOffline: boolean) { + return !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors); +} + export { canEditTaxRate, extractPolicyIDFromPath, @@ -690,6 +698,7 @@ export { hasPolicyErrorFields, hasTaxRateError, isExpensifyTeam, + isDeletedPolicyEmployee, isFreeGroupPolicy, isInstantSubmitEnabled, isPaidGroupPolicy, diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index aea952618071..b23493c08e8e 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -5,10 +5,21 @@ import type {ActivatePhysicalExpensifyCardParams, ReportVirtualExpensifyCardFrau import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ExpensifyCardDetails, IssueNewCardStep} from '@src/types/onyx/Card'; +import type {ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card'; type ReplacementReason = 'damaged' | 'stolen'; +type IssueNewCardFlowData = { + /** Step to be set in Onyx */ + step?: IssueNewCardStep; + + /** Whether the user is editing step */ + isEditing?: boolean; + + /** Data required to be sent to issue a new card */ + data?: Partial; +}; + function reportVirtualExpensifyCardFraud(cardID: number) { const optimisticData: OnyxUpdate[] = [ { @@ -185,9 +196,24 @@ function revealVirtualCardDetails(cardID: number): Promise }); } -function setIssueNewCardStep(step: IssueNewCardStep | null) { - Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {currentStep: step}); +function setIssueNewCardStepAndData({data, isEditing, step}: IssueNewCardFlowData) { + Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {data, isEditing, currentStep: step}); +} + +function clearIssueNewCardFlow() { + Onyx.set(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, { + currentStep: null, + data: {}, + }); } -export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails, setIssueNewCardStep}; +export { + requestReplacementExpensifyCard, + activatePhysicalExpensifyCard, + clearCardListErrors, + reportVirtualExpensifyCardFraud, + revealVirtualCardDetails, + setIssueNewCardStepAndData, + clearIssueNewCardFlow, +}; export type {ReplacementReason}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 936c43b56ee5..adbf5a664c82 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -43,7 +43,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; +import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployeeList, Session} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -304,14 +304,6 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, [route.params.policyID], ); - /** - * Check if the policy member is deleted from the workspace - */ - const isDeletedPolicyEmployee = useCallback( - (policyEmployee: PolicyEmployee): boolean => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors), - [isOffline], - ); - const policyOwner = policy?.owner; const currentUserLogin = currentUserPersonalDetails.login; const invitedPrimaryToSecondaryLogins = invertObject(policy?.primaryLoginsInvited ?? {}); @@ -320,7 +312,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, Object.entries(policy?.employeeList ?? {}).forEach(([email, policyEmployee]) => { const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - if (isDeletedPolicyEmployee(policyEmployee)) { + if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { return; } @@ -375,13 +367,13 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = result.sort((a, b) => (a.text ?? '').toLowerCase().localeCompare((b.text ?? '').toLowerCase())); + result = OptionsListUtils.sortItemsAlphabetically(result); return result; }, [ + isOffline, currentUserLogin, formatPhoneNumber, invitedPrimaryToSecondaryLogins, - isDeletedPolicyEmployee, isPolicyAdmin, personalDetails, policy?.owner, diff --git a/src/pages/workspace/card/issueNew/AssigneeStep.tsx b/src/pages/workspace/card/issueNew/AssigneeStep.tsx index 5012ba294518..23acb3d4a24a 100644 --- a/src/pages/workspace/card/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/card/issueNew/AssigneeStep.tsx @@ -1,30 +1,124 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; -function AssigneeStep() { +const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; + +type AssigneeStepProps = { + // The policy that the card will be issued under + policy: OnyxEntry; +}; + +function AssigneeStep({policy}: AssigneeStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); + + const isEditing = issueNewCard?.isEditing; + + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const submit = () => { - // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309 - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_TYPE); + const submit = (assignee: ListItem) => { + Card.setIssueNewCardStepAndData({ + step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, + data: { + assigneeEmail: assignee?.login ?? '', + }, + isEditing: false, + }); }; const handleBackButtonPress = () => { + if (isEditing) { + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false}); + return; + } Navigation.goBack(); + Card.clearIssueNewCardFlow(); }; + const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; + + const membersDetails = useMemo(() => { + let membersList: ListItem[] = []; + if (!policy?.employeeList) { + return membersList; + } + + Object.entries(policy.employeeList ?? {}).forEach(([email, policyEmployee]) => { + if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { + return; + } + + const personalDetail = PersonalDetailsUtils.getPersonalDetailByEmail(email); + membersList.push({ + keyForList: email, + text: personalDetail?.displayName, + alternateText: email, + login: email, + accountID: personalDetail?.accountID, + icons: [ + { + source: personalDetail?.avatar ?? Expensicons.FallbackAvatar, + name: formatPhoneNumber(email), + type: CONST.ICON_TYPE_AVATAR, + id: personalDetail?.accountID, + }, + ], + }); + }); + + membersList = OptionsListUtils.sortItemsAlphabetically(membersList); + + return membersList; + }, [isOffline, policy?.employeeList]); + + const sections = useMemo(() => { + if (!debouncedSearchTerm) { + return [ + { + data: membersDetails, + shouldShow: true, + }, + ]; + } + + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); + const filteredOptions = membersDetails.filter((option) => !!option.text?.toLowerCase().includes(searchValue) || !!option.alternateText?.toLowerCase().includes(searchValue)); + + return [ + { + title: undefined, + data: filteredOptions, + shouldShow: true, + }, + ]; + }, [membersDetails, debouncedSearchTerm]); + return ( {translate('workspace.card.issueNewCard.whoNeedsCard')} - - {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */} - - + ); } diff --git a/src/pages/workspace/card/issueNew/CardNameStep.tsx b/src/pages/workspace/card/issueNew/CardNameStep.tsx index 9b48d6417732..58b0748e438a 100644 --- a/src/pages/workspace/card/issueNew/CardNameStep.tsx +++ b/src/pages/workspace/card/issueNew/CardNameStep.tsx @@ -1,28 +1,59 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/IssueNewExpensifyCardForm'; function CardNameStep() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {inputCallbackRef} = useAutoFocusInput(); + const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); - const submit = () => { - // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309 - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CONFIRMATION); - }; + const isEditing = issueNewCard?.isEditing; - const handleBackButtonPress = () => { - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT); - }; + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.CARD_TITLE]); + if (!values.cardTitle) { + errors.cardTitle = translate('common.error.fieldRequired'); + } + return errors; + }, + [translate], + ); + + const submit = useCallback((values: FormOnyxValues) => { + Card.setIssueNewCardStepAndData({ + step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, + data: { + cardTitle: values.cardTitle, + }, + isEditing: false, + }); + }, []); + + const handleBackButtonPress = useCallback(() => { + if (isEditing) { + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false}); + return; + } + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.LIMIT}); + }, [isEditing]); return ( {translate('workspace.card.issueNewCard.giveItName')} - {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */} - + ); diff --git a/src/pages/workspace/card/issueNew/CardTypeStep.tsx b/src/pages/workspace/card/issueNew/CardTypeStep.tsx index 93b99f51d239..31b5585b91ad 100644 --- a/src/pages/workspace/card/issueNew/CardTypeStep.tsx +++ b/src/pages/workspace/card/issueNew/CardTypeStep.tsx @@ -1,12 +1,16 @@ import React from 'react'; import {View} from 'react-native'; -import FormProvider from '@components/Form/FormProvider'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; +import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,14 +18,26 @@ import ONYXKEYS from '@src/ONYXKEYS'; function CardTypeStep() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); - const submit = () => { - // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309 - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE); + const isEditing = issueNewCard?.isEditing; + + const submit = (value: ValueOf) => { + Card.setIssueNewCardStepAndData({ + step: isEditing ? CONST.EXPENSIFY_CARD.STEP.CONFIRMATION : CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE, + data: { + cardType: value, + }, + isEditing: false, + }); }; const handleBackButtonPress = () => { - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.ASSIGNEE); + if (isEditing) { + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false}); + return; + } + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.ASSIGNEE}); }; return ( @@ -42,15 +58,32 @@ function CardTypeStep() { /> {translate('workspace.card.issueNewCard.chooseCardType')} - - {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */} - - + + submit(CONST.EXPENSIFY_CARD.CARD_TYPE.PHYSICAL)} + displayInDefaultIconColor + iconStyles={[styles.ml3, styles.mr2]} + iconWidth={variables.menuIconSize} + iconHeight={variables.menuIconSize} + wrapperStyle={styles.purposeMenuItem} + /> + submit(CONST.EXPENSIFY_CARD.CARD_TYPE.VIRTUAL)} + displayInDefaultIconColor + iconStyles={[styles.ml3, styles.mr2]} + iconWidth={variables.menuIconSize} + iconHeight={variables.menuIconSize} + wrapperStyle={styles.purposeMenuItem} + /> + ); } diff --git a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx index a64d6f463531..35f9fab6598f 100644 --- a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx @@ -1,32 +1,62 @@ import React from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {IssueNewCardStep} from '@src/types/onyx/Card'; + +function getTranslationKeyForLimitType(limitType: string | undefined) { + switch (limitType) { + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: + return 'workspace.card.issueNewCard.smartLimit'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: + return 'workspace.card.issueNewCard.fixedAmount'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: + return 'workspace.card.issueNewCard.monthly'; + default: + return ''; + } +} function ConfirmationStep() { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); + const [issueNewCard] = useOnyx(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD); + + const data = issueNewCard?.data; + const submit = () => { - // TODO: the logic will be created in https://github.com/Expensify/App/issues/44309 - Navigation.navigate(ROUTES.SETTINGS); + // TODO: the logic will be created when CreateExpensifyCard is ready + Navigation.goBack(); + Card.clearIssueNewCardFlow(); + }; + + const editStep = (step: IssueNewCardStep) => { + Card.setIssueNewCardStepAndData({step, isEditing: true}); }; const handleBackButtonPress = () => { - Card.setIssueNewCardStep(CONST.EXPENSIFY_CARD.STEP.CARD_NAME); + Card.setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_NAME}); }; + const translationForLimitType = getTranslationKeyForLimitType(data?.limitType); + return ( - {translate('workspace.card.issueNewCard.letsDoubleCheck')} - - {/* TODO: the content will be created in https://github.com/Expensify/App/issues/44309 */} -