diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg
new file mode 100644
index 000000000000..0b85744c1869
--- /dev/null
+++ b/assets/images/product-illustrations/broken-magnifying-glass.svg
@@ -0,0 +1,28 @@
+
diff --git a/src/CONST.ts b/src/CONST.ts
index 5f3fe783f1ac..be7e7fd2088c 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -2291,6 +2291,17 @@ const CONST = {
VISA: 'vcf',
AMEX: 'gl1025',
},
+ STEP_NAMES: ['1', '2', '3', '4'],
+ STEP: {
+ ASSIGNEE: 'Assignee',
+ CARD: 'Card',
+ TRANSACTION_START_DATE: 'TransactionStartDate',
+ CONFIRMATION: 'Confirmation',
+ },
+ TRANSACTION_START_DATE_OPTIONS: {
+ FROM_BEGINNING: 'fromBeginning',
+ CUSTOM: 'custom',
+ },
},
EXPENSIFY_CARD: {
BANK: 'Expensify Card',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index a276f93403bd..d2a0372fd9c7 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -392,6 +392,9 @@ const ONYXKEYS = {
/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',
+ /** Stores the information about the state of assigning a company card */
+ ASSIGN_CARD: 'assignCard',
+
/** Stores the information if mobile selection mode is active */
MOBILE_SELECTION_MODE: 'mobileSelectionMode',
@@ -615,6 +618,8 @@ const ONYXKEYS = {
SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft',
ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard',
ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft',
+ ASSIGN_CARD_FORM: 'assignCard',
+ ASSIGN_CARD_FORM_DRAFT: 'assignCardDraft',
EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName',
EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft',
EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit',
@@ -712,6 +717,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm;
[ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm;
[ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm;
+ [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm;
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm;
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm;
[ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm;
@@ -908,6 +914,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings;
[ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates;
[ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
+ [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard;
[ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode;
[ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 32181bca2bd8..89023063ad8f 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -910,6 +910,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/company-cards',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const,
},
+ WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: {
+ route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card',
+ getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const,
+ },
WORKSPACE_EXPENSIFY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/expensify-card/:cardID',
getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo),
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 829da0575d0a..db790dd389c3 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -368,6 +368,7 @@ const SCREENS = {
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
COMPANY_CARDS: 'Workspace_CompanyCards',
+ COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
COMPANY_CARDS_SETTINGS: 'Workspace_CompanyCards_Settings',
COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name',
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 70cd0168aba8..0616794a8e3a 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -9,6 +9,7 @@ import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg';
import BankUserGreen from '@assets/images/product-illustrations/bank-user--green.svg';
+import BrokenMagnifyingGlass from '@assets/images/product-illustrations/broken-magnifying-glass.svg';
import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue.svg';
import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg';
import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg';
@@ -121,6 +122,7 @@ export {
BankMouseGreen,
BankUserGreen,
BigRocket,
+ BrokenMagnifyingGlass,
ChatBubbles,
CoffeeMug,
ConciergeBlue,
diff --git a/src/components/InteractiveStepWrapper.tsx b/src/components/InteractiveStepWrapper.tsx
new file mode 100644
index 000000000000..6ffe00b9bd5d
--- /dev/null
+++ b/src/components/InteractiveStepWrapper.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import InteractiveStepSubHeader from './InteractiveStepSubHeader';
+import ScreenWrapper from './ScreenWrapper';
+
+type InteractiveStepWrapperProps = {
+ // Step content
+ children: React.ReactNode;
+
+ // ID of the wrapper
+ wrapperID: string;
+
+ // Function to handle back button press
+ handleBackButtonPress: () => void;
+
+ // Title of the back button header
+ headerTitle: string;
+
+ // Index of the highlighted step
+ startStepIndex?: number;
+
+ // Array of step names
+ stepNames?: readonly string[];
+};
+
+function InteractiveStepWrapper({children, wrapperID, handleBackButtonPress, headerTitle, startStepIndex, stepNames}: InteractiveStepWrapperProps) {
+ const styles = useThemeStyles();
+
+ return (
+
+
+ {stepNames && (
+
+
+
+ )}
+ {children}
+
+ );
+}
+
+InteractiveStepWrapper.displayName = 'InteractiveStepWrapper';
+
+export default InteractiveStepWrapper;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 66d1e7c73c90..ca89b782a78e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -10,6 +10,7 @@ import type {
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
+ AssignCardParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
@@ -2792,6 +2793,23 @@ export default {
assignCard: 'Assign card',
cardNumber: 'Card number',
customFeed: 'Custom feed',
+ whoNeedsCardAssigned: 'Who needs a card assigned?',
+ chooseCard: 'Choose a card',
+ chooseCardFor: ({assignee, feed}: AssignCardParams) => `Choose a card for ${assignee} from the ${feed} cards feed.`,
+ noActiveCards: 'No active cards on this feed',
+ somethingMightBeBroken: 'Or something might be broken. Either way, if you have any questions, just',
+ contactConcierge: 'contact Concierge',
+ chooseTransactionStartDate: 'Choose a transaction start date',
+ startDateDescription: 'We will import all transaction from this date onwards. If no date is specified, we’ll go as far back as your bank allows.',
+ fromTheBeginning: 'From the beginning',
+ customStartDate: 'Custom start date',
+ letsDoubleCheck: 'Let’s double check that everything looks right.',
+ confirmationDescription: 'We’ll begin importing transactions immediately.',
+ cardholder: 'Cardholder',
+ card: 'Card',
+ startTransactionDate: 'Start transaction date',
+ cardName: 'Card name',
+ assignedYouCard: (assigner: string) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 54402fd1d6d5..e2e2ba7c2b83 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -8,6 +8,7 @@ import type {
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
+ AssignCardParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
@@ -2839,6 +2840,23 @@ export default {
assignCard: 'Asignar tarjeta',
cardNumber: 'Número de la tarjeta',
customFeed: 'Fuente personalizada',
+ whoNeedsCardAssigned: '¿Quién necesita una tarjeta?',
+ chooseCard: 'Elige una tarjeta',
+ chooseCardFor: ({assignee, feed}: AssignCardParams) => `Elige una tarjeta para ${assignee} del feed de tarjetas ${feed}.`,
+ noActiveCards: 'No hay tarjetas activas en este feed',
+ somethingMightBeBroken: 'O algo podría estar roto. De cualquier manera, si tienes alguna pregunta,',
+ contactConcierge: 'contacta a Concierge',
+ chooseTransactionStartDate: 'Elige una fecha de inicio de transacciones',
+ startDateDescription: 'Importaremos todas las transacciones desde esta fecha en adelante. Si no se especifica una fecha, iremos tan atrás como lo permita tu banco.',
+ fromTheBeginning: 'Desde el principio',
+ customStartDate: 'Fecha de inicio personalizada',
+ letsDoubleCheck: 'Verifiquemos que todo esté bien.',
+ confirmationDescription: 'Comenzaremos a importar transacciones inmediatamente.',
+ cardholder: 'Titular de la tarjeta',
+ card: 'Tarjeta',
+ startTransactionDate: 'Fecha de inicio de transacciones',
+ cardName: 'Nombre de la tarjeta',
+ assignedYouCard: (assigner: string) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index fb396a3f64ea..f953cb17255b 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -361,6 +361,11 @@ type ApprovalWorkflowErrorParams = {
name2: string;
};
+type AssignCardParams = {
+ assignee: string;
+ feed: string;
+};
+
export type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -485,4 +490,5 @@ export type {
RemoveMembersWarningPrompt,
DeleteExpenseTranslationParams,
ApprovalWorkflowErrorParams,
+ AssignCardParams,
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 1bd32b1cd8a7..c2a30f20ed56 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -424,6 +424,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default,
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default,
[SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
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 79d90453b676..22db5deaebfb 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -162,7 +162,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
[SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE],
- [SCREENS.WORKSPACE.COMPANY_CARDS]: [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME],
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: [
+ SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED,
+ SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS,
+ SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME,
+ SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD,
+ ],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 86919a114096..cf45126bfc04 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -517,6 +517,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route,
},
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
+ path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.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 6c0a1f3e4ee6..c8c2c0f0e41d 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1143,6 +1143,10 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.COMPANY_CARDS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
+ policyID: string;
+ feed: string;
+ };
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts
new file mode 100644
index 000000000000..1e58ea3b6306
--- /dev/null
+++ b/src/libs/actions/CompanyCards.ts
@@ -0,0 +1,13 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {AssignCard} from '@src/types/onyx/AssignCard';
+
+function setAssignCardStepAndData({data, isEditing, currentStep}: Partial) {
+ Onyx.merge(ONYXKEYS.ASSIGN_CARD, {data, isEditing, currentStep});
+}
+
+function clearAssignCardStepAndData() {
+ Onyx.set(ONYXKEYS.ASSIGN_CARD, {});
+}
+
+export {setAssignCardStepAndData, clearAssignCardStepAndData};
diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
new file mode 100644
index 000000000000..66ceaaf914c4
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
@@ -0,0 +1,47 @@
+import React, {useEffect} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import * as CompanyCards from '@userActions/CompanyCards';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import AssigneeStep from './AssigneeStep';
+import CardSelectionStep from './CardSelectionStep';
+import ConfirmationStep from './ConfirmationStep';
+import TransactionStartDateStep from './TransactionStartDateStep';
+
+type AssignCardFeedPageProps = {
+ route: {
+ params: {
+ feed: string;
+ };
+ };
+} & WithPolicyAndFullscreenLoadingProps;
+
+function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
+ const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
+ const currentStep = assignCard?.currentStep;
+
+ const feed = route.params?.feed;
+
+ useEffect(() => {
+ CompanyCards.setAssignCardStepAndData({data: {feed}});
+ }, [feed]);
+
+ switch (currentStep) {
+ case CONST.COMPANY_CARD.STEP.ASSIGNEE:
+ return ;
+ case CONST.COMPANY_CARD.STEP.CARD:
+ return ;
+ case CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE:
+ return ;
+ case CONST.COMPANY_CARD.STEP.CONFIRMATION:
+ return ;
+ default:
+ return ;
+ }
+
+ return ;
+}
+
+export default withPolicyAndFullscreenLoading(AssignCardFeedPage);
diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
new file mode 100644
index 000000000000..5be837840390
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
@@ -0,0 +1,174 @@
+import React, {useMemo, useState} from 'react';
+import {Keyboard} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
+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 CompanyCards from '@userActions/CompanyCards';
+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;
+
+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 [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
+
+ const isEditing = assignCard?.isEditing;
+
+ const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? '');
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
+ const [shouldShowError, setShouldShowError] = useState(false);
+
+ const selectMember = (assignee: ListItem) => {
+ Keyboard.dismiss();
+ setSelectedMember(assignee.login ?? '');
+ setShouldShowError(false);
+ };
+
+ const submit = () => {
+ if (!selectedMember) {
+ setShouldShowError(true);
+ return;
+ }
+ CompanyCards.setAssignCardStepAndData({
+ currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.CARD,
+ data: {
+ email: selectedMember,
+ },
+ isEditing: false,
+ });
+ };
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, isEditing: false});
+ return;
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id ?? '-1'));
+ };
+
+ 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,
+ isSelected: selectedMember === email,
+ icons: [
+ {
+ source: personalDetail?.avatar ?? Expensicons.FallbackAvatar,
+ name: formatPhoneNumber(email),
+ type: CONST.ICON_TYPE_AVATAR,
+ id: personalDetail?.accountID,
+ },
+ ],
+ });
+ });
+
+ membersList = OptionsListUtils.sortAlphabetically(membersList, 'text');
+
+ return membersList;
+ }, [isOffline, policy?.employeeList, selectedMember]);
+
+ 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]);
+
+ const headerMessage = useMemo(() => {
+ const searchValue = debouncedSearchTerm.trim().toLowerCase();
+
+ return OptionsListUtils.getHeaderMessage(sections[0].data.length !== 0, false, searchValue);
+ }, [debouncedSearchTerm, sections]);
+
+ return (
+
+ {translate('workspace.companyCards.whoNeedsCardAssigned')}
+
+
+
+ );
+}
+
+AssigneeStep.displayName = 'AssigneeStep';
+
+export default AssigneeStep;
diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx
new file mode 100644
index 000000000000..b31780d54fa2
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx
@@ -0,0 +1,173 @@
+import React, {useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import Icon from '@components/Icon';
+import * as Illustrations from '@components/Icon/Illustrations';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import Text from '@components/Text';
+import TextLink from '@components/TextLink';
+import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as CardUtils from '@libs/CardUtils';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import variables from '@styles/variables';
+import * as CompanyCards from '@userActions/CompanyCards';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+
+type MockedCard = {
+ key: string;
+ cardNumber: string;
+};
+
+const mockedCardList = [
+ {
+ key: '1',
+ cardNumber: '123412XXXXXX1234',
+ },
+ {
+ key: '2',
+ cardNumber: '123412XXXXXX1235',
+ },
+ {
+ key: '3',
+ cardNumber: '123412XXXXXX1236',
+ },
+];
+
+const mockedCardListEmpty: MockedCard[] = [];
+
+const feedNamesMapping = {
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: 'Visa',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: 'MasterCard',
+ [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: 'American Express',
+};
+
+type CardSelectionStepProps = {
+ feed: string;
+};
+
+function CardSelectionStep({feed}: CardSelectionStepProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {environmentURL} = useEnvironment();
+ const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
+
+ const isEditing = assignCard?.isEditing;
+ const assignee = assignCard?.data?.email ?? '';
+
+ const [cardSelected, setCardSelected] = useState(assignCard?.data?.cardName ?? '');
+ const [shouldShowError, setShouldShowError] = useState(false);
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ CompanyCards.setAssignCardStepAndData({
+ currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION,
+ isEditing: false,
+ });
+ return;
+ }
+ CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE});
+ };
+
+ const handleSelectCard = (cardNumber: string) => {
+ setCardSelected(cardNumber);
+ setShouldShowError(false);
+ };
+
+ const submit = () => {
+ if (!cardSelected) {
+ setShouldShowError(true);
+ return;
+ }
+ CompanyCards.setAssignCardStepAndData({
+ currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE,
+ data: {cardName: cardSelected},
+ isEditing: false,
+ });
+ };
+
+ // TODO: for now mocking cards
+ const mockedCards = !Object.values(CONST.COMPANY_CARD.FEED_BANK_NAME).some((value) => value === feed) ? mockedCardListEmpty : mockedCardList;
+
+ const cardListOptions = mockedCards.map((item) => ({
+ keyForList: item?.cardNumber,
+ value: item?.cardNumber,
+ text: item?.cardNumber,
+ isSelected: cardSelected === item?.cardNumber,
+ leftElement: (
+
+ ),
+ }));
+
+ return (
+
+ {!cardListOptions.length ? (
+
+
+ {translate('workspace.companyCards.noActiveCards')}
+
+ {translate('workspace.companyCards.somethingMightBeBroken')}{' '}
+
+ {translate('workspace.companyCards.contactConcierge')}
+
+ .
+
+
+ ) : (
+ <>
+ {translate('workspace.companyCards.chooseCard')}
+
+ {translate('workspace.companyCards.chooseCardFor', {
+ assignee: PersonalDetailsUtils.getPersonalDetailByEmail(assignee ?? '')?.displayName ?? '',
+ feed: feedNamesMapping[feed as ValueOf] ?? 'visa',
+ })}
+
+ handleSelectCard(value)}
+ initiallyFocusedOptionKey={cardSelected}
+ shouldUpdateFocusedIndex
+ />
+
+ >
+ )}
+
+ );
+}
+
+CardSelectionStep.displayName = 'CardSelectionStep';
+
+export default CardSelectionStep;
diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx
new file mode 100644
index 000000000000..5eb39719c022
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import ScrollView from '@components/ScrollView';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import Navigation from '@navigation/Navigation';
+import * as CompanyCards from '@userActions/CompanyCards';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {AssignCardStep} from '@src/types/onyx/AssignCard';
+
+function ConfirmationStep() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+
+ const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
+ const safePaddingBottomStyle = useSafePaddingBottomStyle();
+
+ const data = assignCard?.data;
+
+ const submit = () => {
+ Navigation.goBack();
+ CompanyCards.clearAssignCardStepAndData();
+ };
+
+ const editStep = (step: AssignCardStep) => {
+ CompanyCards.setAssignCardStepAndData({currentStep: step, isEditing: true});
+ };
+
+ const handleBackButtonPress = () => {
+ CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE});
+ };
+
+ return (
+
+
+ {translate('workspace.companyCards.letsDoubleCheck')}
+ {translate('workspace.companyCards.confirmationDescription')}
+ editStep(CONST.COMPANY_CARD.STEP.ASSIGNEE)}
+ />
+ editStep(CONST.COMPANY_CARD.STEP.CARD)}
+ />
+ editStep(CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE)}
+ />
+
+
+
+
+
+ );
+}
+
+ConfirmationStep.displayName = 'ConfirmationStep';
+
+export default ConfirmationStep;
diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx
new file mode 100644
index 000000000000..8c0990800bb4
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateSelectorModal.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import DatePicker from '@components/DatePicker';
+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 Modal from '@components/Modal';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import INPUT_IDS from '@src/types/form/AssignCardForm';
+
+type TransactionStartDateSelectorModalProps = {
+ /** Whether the modal is visible */
+ isVisible: boolean;
+
+ /** The date to display in the date picker */
+ date: string;
+
+ /** Function to call when the user selects a date */
+ handleSelectDate: (date: string) => void;
+
+ /** Function to call when the user closes the type selector modal */
+ onClose: () => void;
+};
+
+function TransactionStartDateSelectorModal({isVisible, date, handleSelectDate, onClose}: TransactionStartDateSelectorModalProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const validate = (values: FormOnyxValues): FormInputErrors =>
+ ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.START_DATE]);
+
+ const submit = (values: FormOnyxValues) => {
+ handleSelectDate(values[INPUT_IDS.START_DATE]);
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+TransactionStartDateSelectorModal.displayName = 'TransactionStartDateSelectorModal';
+
+export default TransactionStartDateSelectorModal;
diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx
new file mode 100644
index 000000000000..cfdb09e3df83
--- /dev/null
+++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx
@@ -0,0 +1,136 @@
+import React, {useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle';
+import useThemeStyles from '@hooks/useThemeStyles';
+import DateUtils from '@libs/DateUtils';
+import * as CompanyCards from '@userActions/CompanyCards';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import TransactionStartDateSelectorModal from './TransactionStartDateSelectorModal';
+
+function TransactionStartDateStep() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const safePaddingBottomStyle = useSafePaddingBottomStyle();
+
+ const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
+ const isEditing = assignCard?.isEditing;
+ const data = assignCard?.data;
+
+ const [dateOptionSelected, setDateOptionSelected] = useState(data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING);
+ const [isModalOpened, setIsModalOpened] = useState(false);
+ const [startDate, setStartDate] = useState(DateUtils.extractDate(new Date().toString()));
+
+ const handleBackButtonPress = () => {
+ if (isEditing) {
+ CompanyCards.setAssignCardStepAndData({
+ currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION,
+ isEditing: false,
+ });
+ return;
+ }
+ CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CARD});
+ };
+
+ const handleSelectDate = (date: string) => {
+ setStartDate(date);
+ };
+
+ const handleSelectDateOption = (dateOption: string) => {
+ setDateOptionSelected(dateOption);
+ if (dateOption === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING) {
+ const newStartDate = new Date();
+ setStartDate(DateUtils.extractDate(newStartDate.toString()));
+ }
+ };
+
+ const submit = () => {
+ CompanyCards.setAssignCardStepAndData({
+ currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION,
+ data: {
+ dateOption: dateOptionSelected,
+ startDate,
+ },
+ isEditing: false,
+ });
+ };
+
+ const dateOptions = useMemo(
+ () => [
+ {
+ value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING,
+ text: translate('workspace.companyCards.fromTheBeginning'),
+ keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING,
+ isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING,
+ },
+ {
+ value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM,
+ text: translate('workspace.companyCards.customStartDate'),
+ keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM,
+ isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM,
+ },
+ ],
+ [dateOptionSelected, translate],
+ );
+
+ return (
+
+ {translate('workspace.companyCards.chooseTransactionStartDate')}
+ {translate('workspace.companyCards.startDateDescription')}
+
+ handleSelectDateOption(value)}
+ sections={[{data: dateOptions}]}
+ shouldSingleExecuteRowSelect
+ initiallyFocusedOptionKey={dateOptionSelected}
+ shouldUpdateFocusedIndex
+ containerStyle={[styles.flex0, styles.flexShrink0, styles.flexBasisAuto, styles.pb0]}
+ // containerStyle={[styles.flexReset, styles.pb0]}
+ />
+ {dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM && (
+ <>
+ setIsModalOpened(true)}
+ />
+ setIsModalOpened(false)}
+ />
+ >
+ )}
+
+
+
+ );
+}
+
+TransactionStartDateStep.displayName = 'TransactionStartDateStep';
+
+export default TransactionStartDateStep;
diff --git a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
index ef63edcb493f..c5a852450828 100644
--- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
@@ -82,7 +82,7 @@ function ConfirmationStep({policyID, backTo}: ConfirmationStepProps) {
style={styles.pt0}
contentContainerStyle={styles.flexGrow1}
>
- {translate('workspace.card.issueNewCard.letsDoubleCheck')}
+ {translate('workspace.card.issueNewCard.letsDoubleCheck')}
{translate('workspace.card.issueNewCard.willBeReady')}
;
+
+type AssignCardForm = Form<
+ InputID,
+ {
+ [INPUT_IDS.START_DATE]: string;
+ }
+>;
+
+export type {AssignCardForm};
+export default INPUT_IDS;
diff --git a/src/types/form/index.ts b/src/types/form/index.ts
index 3b9f24a7ed98..94b6a4ec08a4 100644
--- a/src/types/form/index.ts
+++ b/src/types/form/index.ts
@@ -1,4 +1,5 @@
export type {AddPaymentCardForm} from './AddPaymentCardForm';
+export type {AssignCardForm} from './AssignCardForm';
export type {CloseAccountForm} from './CloseAccountForm';
export type {DateOfBirthForm} from './DateOfBirthForm';
export type {DisplayNameForm} from './DisplayNameForm';
diff --git a/src/types/onyx/AssignCard.ts b/src/types/onyx/AssignCard.ts
new file mode 100644
index 000000000000..0b2592d15079
--- /dev/null
+++ b/src/types/onyx/AssignCard.ts
@@ -0,0 +1,40 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+/** Assign card flow steps */
+type AssignCardStep = ValueOf;
+
+/** Data required to be sent to issue a new card */
+type AssignCardData = {
+ /** The email address of the asignee */
+ email: string;
+
+ /** Number of the selected card */
+ cardNumber: string;
+
+ /** The name of the feed */
+ feed: string;
+
+ /** The name of the card */
+ cardName: string;
+
+ /** The transaction start date of the card */
+ startDate: string;
+
+ /** An option based on which the transaction start date is chosen */
+ dateOption: string;
+};
+
+/** Model of assign card flow */
+type AssignCard = {
+ /** The current step of the flow */
+ currentStep: AssignCardStep;
+
+ /** Data required to be sent to assign a card */
+ data: Partial;
+
+ /** Whether the user is editing step */
+ isEditing: boolean;
+};
+
+export type {AssignCard, AssignCardStep, AssignCardData};
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 4b174e6a4dc7..0073e47bb65c 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -1,6 +1,7 @@
import type Account from './Account';
import type AccountData from './AccountData';
import type {ApprovalWorkflowOnyx} from './ApprovalWorkflow';
+import type {AssignCard} from './AssignCard';
import type {BankAccountList} from './BankAccount';
import type BankAccount from './BankAccount';
import type Beta from './Beta';
@@ -108,6 +109,7 @@ export type {
TryNewDot,
Account,
AccountData,
+ AssignCard,
BankAccount,
BankAccountList,
Beta,