From 9af814030336a3dfd407488d1f459e08915d2803 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 8 May 2025 09:11:32 +0000 Subject: [PATCH 01/13] introduce `shouldInviteAssigneeToWorkspace` --- src/libs/actions/Card.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index b380d1c9e153b..b646bb061904a 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -38,6 +38,9 @@ type IssueNewCardFlowData = { /** ID of the policy */ policyID: string | undefined; + + /** Should invite the assignee to workspace */ + shouldInviteAssigneeToWorkspace?: boolean; }; function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) { @@ -354,12 +357,13 @@ function getCardDefaultName(userName?: string) { return `${userName}'s Card`; } -function setIssueNewCardStepAndData({data, isEditing, step, policyID}: IssueNewCardFlowData) { +function setIssueNewCardStepAndData({data, isEditing, step, policyID, shouldInviteAssigneeToWorkspace}: IssueNewCardFlowData) { Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, { data, isEditing, currentStep: step, errors: null, + shouldInviteAssigneeToWorkspace, }); } From a93867d6c5f3c41615611d2eb1b18a1d094d7e5b Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 14:42:58 +0000 Subject: [PATCH 02/13] add invite flow to expensify cards --- src/CONST.ts | 1 + src/ROUTES.ts | 2 +- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Policy/Policy.ts | 2 +- .../expensifyCard/issueNew/AssigneeStep.tsx | 114 ++++++++-- .../issueNew/InviteeNewMemberStep.tsx | 213 ++++++++++++++++++ .../issueNew/IssueNewCardPage.tsx | 3 + src/types/onyx/Card.ts | 3 + 9 files changed, 323 insertions(+), 17 deletions(-) create mode 100644 src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 871450f669d08..e27146d9e0eb0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3277,6 +3277,7 @@ const CONST = { STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', + INVITE_NEW_MEMBER: 'InviteNewMember', CARD_TYPE: 'CardType', LIMIT_TYPE: 'LimitType', LIMIT: 'Limit', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 526a16097e2eb..7b2421a29f008 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -975,7 +975,7 @@ const ROUTES = { }, WORKSPACE_INVITE_MESSAGE_ROLE: { route: 'settings/workspaces/:policyID/invite-message/role', - getRoute: (policyID: string, backTo?: string) => `${getUrlWithBackToParam(`settings/workspaces/${policyID}/invite-message/role`, backTo)}` as const, + getRoute: (policyID: string | undefined, backTo?: string) => `${getUrlWithBackToParam(`settings/workspaces/${policyID}/invite-message/role`, backTo)}` as const, }, WORKSPACE_OVERVIEW: { route: 'settings/workspaces/:policyID/overview', diff --git a/src/languages/en.ts b/src/languages/en.ts index bf25be5bd7700..2d1b184370a8e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4641,6 +4641,7 @@ const translations = { invite: { member: 'Invite member', members: 'Invite members', + inviteNewMember: 'Invite new member', invitePeople: 'Invite new members', genericFailureMessage: 'An error occurred while inviting the member to the workspace. Please try again.', pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 0e48eee26d1b4..c45ce8b09338c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4687,6 +4687,7 @@ const translations = { invite: { member: 'Invitar miembros', members: 'Invitar miembros', + inviteNewMember: 'Invitar nuevo miembro', invitePeople: 'Invitar nuevos miembros', genericFailureMessage: 'Se ha producido un error al invitar al miembro al espacio de trabajo. Vuelva a intentarlo.', pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 0cd2b8155c8af..2f6137ed806fb 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2472,7 +2472,7 @@ function updateMemberCustomField(policyID: string, login: string, customFieldTyp API.write(WRITE_COMMANDS.UPDATE_POLICY_MEMBERS_CUSTOM_FIELDS, params, {optimisticData, successData, failureData}); } -function setWorkspaceInviteMessageDraft(policyID: string, message: string | null) { +function setWorkspaceInviteMessageDraft(policyID: string | undefined, message: string | null) { Onyx.set(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, message); } diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index e8978af095c6d..99c592c89a216 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -1,8 +1,10 @@ -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import {useBetas} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; @@ -11,8 +13,9 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {searchInServer} from '@libs/actions/Report'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; -import {getHeaderMessage, getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; +import {filterAndOrderOptions, getHeaderMessage, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail, getUserNameByEmail} from '@libs/PersonalDetailsUtils'; import {isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; @@ -29,6 +32,70 @@ type AssigneeStepProps = { policy: OnyxEntry; }; +function useOptions() { + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const existingDelegates = useMemo( + () => + account?.delegatedAccess?.delegates?.reduce((prev, {email}) => { + // eslint-disable-next-line no-param-reassign + prev[email] = true; + return prev; + }, {} as Record), + [account?.delegatedAccess?.delegates], + ); + + const defaultOptions = useMemo(() => { + const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions, existingDelegates]); + + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; +} + function AssigneeStep({policy}: AssigneeStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -36,15 +103,24 @@ function AssigneeStep({policy}: AssigneeStepProps) { const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); + const {userToInvite, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const isEditing = issueNewCard?.isEditing; - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const submit = (assignee: ListItem) => { const data: Partial = { assigneeEmail: assignee?.login ?? '', }; + if (userToInvite?.accountID === assignee?.accountID) { + data.assigneeAccountID = assignee?.accountID; + setIssueNewCardStepAndData({ + step: CONST.EXPENSIFY_CARD.STEP.INVITE_NEW_MEMBER, + data, + policyID, + }); + return; + } + if (isEditing && issueNewCard?.data?.cardTitle === getCardDefaultName(getUserNameByEmail(issueNewCard?.data?.assigneeEmail, 'firstName'))) { // If the card title is the default card title, update it with the new assignee's name data.cardTitle = getCardDefaultName(getUserNameByEmail(assignee?.login ?? '', 'firstName')); @@ -105,16 +181,16 @@ function AssigneeStep({policy}: AssigneeStepProps) { }, [isOffline, policy?.employeeList]); const sections = useMemo(() => { - if (!debouncedSearchTerm) { + if (!debouncedSearchValue) { return [ { data: membersDetails, shouldShow: true, + isDisabled: undefined, }, ]; } - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); const filteredOptions = membersDetails.filter((option) => !!option.text?.toLowerCase().includes(searchValue) || !!option.alternateText?.toLowerCase().includes(searchValue)); return [ @@ -122,15 +198,23 @@ function AssigneeStep({policy}: AssigneeStepProps) { title: undefined, data: filteredOptions, shouldShow: true, + isDisabled: undefined, }, + ...(userToInvite + ? [ + { + title: undefined, + data: [userToInvite], + shouldShow: true, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); + }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); - return getHeaderMessage(sections[0].data.length !== 0, false, searchValue); - }, [debouncedSearchTerm, sections]); + useEffect(() => { + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); return ( {translate('workspace.card.issueNewCard.whoNeedsCard')} ; +}; + +function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [welcomeNote, setWelcomeNote] = useState(); + const policyID = policy?.id; + const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, { + canBeMissing: true, + }); + const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); + const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true}); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, formDataResult); + + const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID}`, {canBeMissing: true}); + const getDefaultWelcomeNote = useCallback(() => { + return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft ?? + translate('workspace.common.welcomeNote') + ); + }, [workspaceInviteMessageDraft, translate, formData]); + + const welcomeNoteSubject = useMemo( + () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, + [policy?.name, currentUserPersonalDetails?.displayName], + ); + + useEffect(() => { + if (isOnyxLoading) { + return; + } + + if (!isEmptyObject(issueNewCard?.data?.assigneeEmail)) { + setWelcomeNote(getDefaultWelcomeNote()); + } + + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isOnyxLoading]); + + const {inputCallbackRef, inputRef} = useAutoFocusInput(); + + const isEditing = issueNewCard?.isEditing; + const handleBackButtonPress = () => { + if (isEditing) { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false, policyID}); + return; + } + + setIssueNewCardStepAndData({ + step: CONST.EXPENSIFY_CARD.STEP.ASSIGNEE, + data: {...issueNewCard?.data, assigneeAccountID: undefined, assigneeEmail: undefined}, + isEditing: false, + policyID, + }); + }; + + const sendInvitationAndGoToNextStep = () => { + Keyboard.dismiss(); + const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details + addMembersToWorkspace( + { + [issueNewCard?.data?.assigneeEmail ?? '']: issueNewCard?.data?.assigneeAccountID ?? 0, + }, + `${welcomeNoteSubject}\n\n${welcomeNote}`, + policyID ?? '', + policyMemberAccountIDs, + workspaceInviteRoleDraft, + ); + setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); + clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); + + if (isEditing) { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false, policyID}); + } else { + setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, isEditing: false, policyID}); + } + }; + + useEffect(() => { + return () => { + clearWorkspaceInviteRoleDraft(policyID ?? ''); + }; + }, [policyID]); + + return ( + + {translate('workspace.invite.inviteNewMember')} + { + const errorFields: FormInputErrors = {}; + if (isEmptyObject(issueNewCard?.data.assigneeEmail)) { + errorFields.cardTitle = translate('workspace.inviteMessage.inviteNoMembersError'); + } + return errorFields; + }} + style={[styles.mh5, styles.flexGrow1]} + enabledWhenOffline + shouldHideFixErrorsAlert + addBottomSafeAreaPadding + > + + + + + {translate('workspace.inviteMessage.inviteMessagePrompt')} + + + + { + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID, Navigation.getActiveRoute())); + }} + /> + + { + setWelcomeNote(text); + }} + ref={(element: AnimatedTextInputRef) => { + if (!element) { + return; + } + if (!inputRef.current) { + updateMultilineInputRange(element); + } + inputCallbackRef(element); + }} + shouldSaveDraft + /> + + + + ); +} + +InviteNewMemberStep.displayName = 'InviteNewMemberStep'; + +export default InviteNewMemberStep; diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx index e845e3cf22bc2..ac6da083b0d35 100644 --- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx @@ -17,6 +17,7 @@ import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardTypeStep from './CardTypeStep'; import ConfirmationStep from './ConfirmationStep'; +import InviteNewMemberStep from './InviteeNewMemberStep'; import LimitStep from './LimitStep'; import LimitTypeStep from './LimitTypeStep'; @@ -39,6 +40,8 @@ function IssueNewCardPage({policy, route}: IssueNewCardPageProps) { switch (currentStep) { case CONST.EXPENSIFY_CARD.STEP.ASSIGNEE: return ; + case CONST.EXPENSIFY_CARD.STEP.INVITE_NEW_MEMBER: + return ; case CONST.EXPENSIFY_CARD.STEP.CARD_TYPE: return ; case CONST.EXPENSIFY_CARD.STEP.LIMIT_TYPE: diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index 1ddcd51a9f68c..784bcb72cd594 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -212,6 +212,9 @@ type IssueNewCardData = { /** The email address of the cardholder */ assigneeEmail: string; + /** The account ID of the cardholder */ + assigneeAccountID?: number; + /** Card type */ cardType: ValueOf; From da23f2a011e653f4bbbae1c03c61c19beae4db26 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 14:53:07 +0000 Subject: [PATCH 03/13] fix esLint --- src/libs/actions/Policy/Member.ts | 9 ++++++-- .../expensifyCard/issueNew/AssigneeStep.tsx | 21 ++++++------------- .../issueNew/InviteeNewMemberStep.tsx | 10 ++++----- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 384b8530ed3f2..0dadbf8103427 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -931,7 +931,12 @@ function buildAddMembersToWorkspaceOnyxData(invitedEmailsToAccountIDs: InvitedEm * Adds members to the specified workspace/policyID * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ -function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string, policyMemberAccountIDs: number[], role: string) { +function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs, welcomeNote: string, policyID: string | undefined, policyMemberAccountIDs: number[], role: string) { + if (!policyID) { + Log.warn('addMembersToWorkspace missing policyID', {invitedEmailsToAccountIDs, welcomeNote, policyMemberAccountIDs, role}); + return; + } + const {optimisticData, successData, failureData, optimisticAnnounceChat, membersChats, logins} = buildAddMembersToWorkspaceOnyxData( invitedEmailsToAccountIDs, policyID, @@ -1127,7 +1132,7 @@ function setWorkspaceInviteRoleDraft(policyID: string, role: ValueOf - account?.delegatedAccess?.delegates?.reduce((prev, {email}) => { - // eslint-disable-next-line no-param-reassign - prev[email] = true; - return prev; - }, {} as Record), - [account?.delegatedAccess?.delegates], - ); const defaultOptions = useMemo(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( @@ -56,7 +46,7 @@ function useOptions() { }, { betas, - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT}, }, ); @@ -74,11 +64,11 @@ function useOptions() { currentUserOption, headerMessage, }; - }, [optionsList.reports, optionsList.personalDetails, betas, existingDelegates, isLoading]); + }, [optionsList.reports, optionsList.personalDetails, betas, isLoading]); const options = useMemo(() => { const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { - excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT, ...existingDelegates}, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT}, maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, }); const headerMessage = getHeaderMessage( @@ -91,7 +81,7 @@ function useOptions() { ...filteredOptions, headerMessage, }; - }, [debouncedSearchValue, defaultOptions, existingDelegates]); + }, [debouncedSearchValue, defaultOptions]); return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; } @@ -101,7 +91,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const policyID = policy?.id; - const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); + const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); const {userToInvite, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const isEditing = issueNewCard?.isEditing; @@ -181,6 +171,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { }, [isOffline, policy?.employeeList]); const sections = useMemo(() => { + if (!debouncedSearchValue) { return [ { diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx index 6869db9feb6df..be1fc6f0c8597 100644 --- a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx @@ -34,18 +34,18 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; type InviteeNewMemberStepProps = { // The policy that the card will be issued under - policy?: OnyxEntry; + policy: OnyxEntry; }; function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [welcomeNote, setWelcomeNote] = useState(); - const policyID = policy?.id; + const policyID = policy.id; const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, { canBeMissing: true, }); - const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); + const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -103,10 +103,10 @@ function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details addMembersToWorkspace( { - [issueNewCard?.data?.assigneeEmail ?? '']: issueNewCard?.data?.assigneeAccountID ?? 0, + [issueNewCard?.data?.assigneeEmail ?? '']: issueNewCard?.data?.assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID, }, `${welcomeNoteSubject}\n\n${welcomeNote}`, - policyID ?? '', + policyID, policyMemberAccountIDs, workspaceInviteRoleDraft, ); From e6929283c395d8fe8988f84caf99b61db01c7796 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 20:28:24 +0530 Subject: [PATCH 04/13] Update src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx --- .../workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx index be1fc6f0c8597..8a94e54d993b0 100644 --- a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx @@ -41,7 +41,7 @@ function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [welcomeNote, setWelcomeNote] = useState(); - const policyID = policy.id; + const policyID = policy?.id; const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, { canBeMissing: true, }); From 9d42e1a716cc85faa260cc10771e33de889cb7f5 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 20:28:30 +0530 Subject: [PATCH 05/13] Update src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx --- .../workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx index 8a94e54d993b0..8d3f7d748b14b 100644 --- a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx @@ -122,7 +122,7 @@ function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { useEffect(() => { return () => { - clearWorkspaceInviteRoleDraft(policyID ?? ''); + clearWorkspaceInviteRoleDraft(policyID); }; }, [policyID]); From bd6765ed9043c25bf145ef8fffb0fbb49f7a2fbd Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 15:08:00 +0000 Subject: [PATCH 06/13] fix prettier and remove unwanted const --- src/libs/actions/Card.ts | 6 +----- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 85004f8a45628..d147406c64905 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -40,9 +40,6 @@ type IssueNewCardFlowData = { /** ID of the policy */ policyID: string | undefined; - - /** Should invite the assignee to workspace */ - shouldInviteAssigneeToWorkspace?: boolean; }; function reportVirtualExpensifyCardFraud(card: Card, validateCode: string) { @@ -366,13 +363,12 @@ function getCardDefaultName(userName?: string) { return `${userName}'s Card`; } -function setIssueNewCardStepAndData({data, isEditing, step, policyID, shouldInviteAssigneeToWorkspace}: IssueNewCardFlowData) { +function setIssueNewCardStepAndData({data, isEditing, step, policyID}: IssueNewCardFlowData) { Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, { data, isEditing, currentStep: step, errors: null, - shouldInviteAssigneeToWorkspace, }); } diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index cd6d136dc2c76..02e5dcf9fe739 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -171,7 +171,6 @@ function AssigneeStep({policy}: AssigneeStepProps) { }, [isOffline, policy?.employeeList]); const sections = useMemo(() => { - if (!debouncedSearchValue) { return [ { From f9b7b73b4b265f906da17da0173275d0b7838600 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 16:36:30 +0000 Subject: [PATCH 07/13] aaply fix for company card --- src/CONST.ts | 2 + .../assignCard/AssignCardFeedPage.tsx | 14 +- .../companyCards/assignCard/AssigneeStep.tsx | 102 ++++++-- .../assignCard/ConfirmationStep.tsx | 2 +- .../assignCard/InviteNewMemberStep.tsx | 227 ++++++++++++++++++ ...MemberStep.tsx => InviteNewMemberStep.tsx} | 0 .../issueNew/IssueNewCardPage.tsx | 2 +- src/types/onyx/AssignCard.ts | 3 + 8 files changed, 334 insertions(+), 18 deletions(-) create mode 100644 src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx rename src/pages/workspace/expensifyCard/issueNew/{InviteeNewMemberStep.tsx => InviteNewMemberStep.tsx} (100%) diff --git a/src/CONST.ts b/src/CONST.ts index e27146d9e0eb0..38215c1f23148 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3235,6 +3235,7 @@ const CONST = { STEP: { BANK_CONNECTION: 'BankConnection', ASSIGNEE: 'Assignee', + INVITE_NEW_MEMBER: 'InviteNewMember', CARD: 'Card', CARD_NAME: 'CardName', TRANSACTION_START_DATE: 'TransactionStartDate', @@ -3278,6 +3279,7 @@ const CONST = { STEP: { ASSIGNEE: 'Assignee', INVITE_NEW_MEMBER: 'InviteNewMember', + CARD_TYPE: 'CardType', LIMIT_TYPE: 'LimitType', LIMIT: 'Limit', diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 234c6000d1150..5f9c1fbd4b9b5 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -1,9 +1,13 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; +import {useBetas} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; +import useDebouncedState from '@hooks/useDebouncedState'; import useInitial from '@hooks/useInitial'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import BankConnection from '@pages/workspace/companyCards/BankConnection'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -17,6 +21,7 @@ import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardSelectionStep from './CardSelectionStep'; import ConfirmationStep from './ConfirmationStep'; +import InviteNewMemberStep from './InviteNewMemberStep'; import TransactionStartDateStep from './TransactionStartDateStep'; type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps; @@ -65,6 +70,13 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { feed={feed} /> ); + case CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER: + return ( + + ); case CONST.COMPANY_CARD.STEP.CARD: return ( { + const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT}, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + excludeLogins: {...CONST.EXPENSIFY_EMAILS_OBJECT}, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions]); + + return {...options, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized}; +} + function AssigneeStep({policy, feed}: AssigneeStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -49,13 +106,23 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { const isEditing = assignCard?.isEditing; const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {userToInvite, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const [shouldShowError, setShouldShowError] = useState(false); const selectMember = (assignee: ListItem) => { Keyboard.dismiss(); setSelectedMember(assignee.login ?? ''); setShouldShowError(false); + + if (userToInvite?.login === assignee.login) { + setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.INVITE_NEW_MEMBER, + data: { + email: assignee?.login, + assigneeAccountID: assignee?.accountID ?? CONST.DEFAULT_NUMBER_ID, + }, + }); + } }; const submit = () => { @@ -143,7 +210,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, [isOffline, policy?.employeeList, selectedMember]); const sections = useMemo(() => { - if (!debouncedSearchTerm) { + if (!debouncedSearchValue) { return [ { data: membersDetails, @@ -151,8 +218,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, ]; } - - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); const filteredOptions = membersDetails.filter((option) => !!option.text?.toLowerCase().includes(searchValue) || !!option.alternateText?.toLowerCase().includes(searchValue)); return [ @@ -161,14 +226,21 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { data: filteredOptions, shouldShow: true, }, + ...(userToInvite + ? [ + { + title: undefined, + data: [userToInvite], + shouldShow: true, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); + }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); - return getHeaderMessage(sections[0].data.length !== 0, false, searchValue); - }, [debouncedSearchTerm, sections]); + useEffect(() => { + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); return ( {translate('workspace.companyCards.whoNeedsCardAssigned')} { if (!assignCard?.isAssigned) { diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx new file mode 100644 index 0000000000000..f22a35005ec1c --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -0,0 +1,227 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {Keyboard, View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import MultipleAvatars from '@components/MultipleAvatars'; +import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useCardFeeds from '@hooks/useCardFeeds'; +import useCardsList from '@hooks/useCardsList'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import {addMembersToWorkspace, clearWorkspaceInviteRoleDraft} from '@libs/actions/Policy/Member'; +import {setWorkspaceInviteMessageDraft} from '@libs/actions/Policy/Policy'; +import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; +import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; +import updateMultilineInputRange from '@libs/updateMultilineInputRange'; +import variables from '@styles/variables'; +import {setAssignCardStepAndData} from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/WorkspaceInviteMessageForm'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; + +type InviteNewMemberStepProps = { + policy: OnyxEntry; + feed: OnyxTypes.CompanyCardFeed; +}; + +function InviteNewMemberStep({policy, feed}: InviteNewMemberStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [welcomeNote, setWelcomeNote] = useState(); + const policyID = policy?.id; + + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); + const [workspaceInviteMessageDraft, workspaceInviteMessageDraftResult] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT}${policyID}`, {canBeMissing: true}); + const [formData, formDataResult] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT, {canBeMissing: true}); + const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID}`, {canBeMissing: true}); + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); + + const [list] = useCardsList(policy?.id, feed); + const [cardFeeds] = useCardFeeds(policy?.id); + const filteredCardList = getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[feed]); + const isOnyxLoading = isLoadingOnyxValue(workspaceInviteMessageDraftResult, formDataResult); + + const getDefaultWelcomeNote = useCallback(() => { + return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? + // workspaceInviteMessageDraft can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + workspaceInviteMessageDraft ?? + translate('workspace.common.welcomeNote') + ); + }, [workspaceInviteMessageDraft, translate, formData]); + + const welcomeNoteSubject = useMemo( + () => `# ${currentUserPersonalDetails?.displayName ?? ''} invited you to ${policy?.name ?? 'a workspace'}`, + [policy?.name, currentUserPersonalDetails?.displayName], + ); + + useEffect(() => { + if (isOnyxLoading) { + return; + } + + if (!isEmptyObject(assignCard?.data?.email)) { + setWelcomeNote(getDefaultWelcomeNote()); + } + }, [assignCard?.data?.email, getDefaultWelcomeNote, isOnyxLoading]); + + const {inputCallbackRef, inputRef} = useAutoFocusInput(); + const isEditing = assignCard?.isEditing; + + const handleBackButtonPress = () => { + if (isEditing) { + setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, isEditing: false}); + } else { + setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, + data: { + ...assignCard?.data, + assigneeAccountID: undefined, + email: undefined, + }, + isEditing: false, + }); + } + }; + + const sendInvitationAndGoToNextStep = () => { + Keyboard.dismiss(); + const memberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + + addMembersToWorkspace( + { + [assignCard?.data?.email ?? '']: assignCard?.data?.assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID, + }, + `${welcomeNoteSubject}\n\n${welcomeNote}`, + policyID, + memberAccountIDs, + workspaceInviteRoleDraft, + ); + + setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); + clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); + + let nextStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; + const data: Partial = { + email: assignCard?.data?.email, + cardName: getDefaultCardName(assignCard?.data?.email), + }; + + if (hasOnlyOneCardToAssign(filteredCardList)) { + nextStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; + data.cardNumber = Object.keys(filteredCardList).at(0); + data.encryptedCardNumber = Object.values(filteredCardList).at(0); + } + + setAssignCardStepAndData({ + currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep, + data, + isEditing: false, + }); + }; + + useEffect(() => { + return () => { + clearWorkspaceInviteRoleDraft(policyID); + }; + }, [policyID]); + + return ( + + {translate('workspace.companyCards.whoNeedsCardAssigned')} + + + + + + + + {translate('workspace.inviteMessage.inviteMessagePrompt')} + + + + + { + Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE_ROLE.getRoute(policyID, Navigation.getActiveRoute())); + }} + /> + + + { + if (!element) { + return; + } + if (!inputRef.current) { + updateMultilineInputRange(element); + } + inputCallbackRef(element); + }} + shouldSaveDraft + /> + + + + ); +} + +InviteNewMemberStep.displayName = 'InviteNewMemberStep'; + +export default InviteNewMemberStep; diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx similarity index 100% rename from src/pages/workspace/expensifyCard/issueNew/InviteeNewMemberStep.tsx rename to src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx index ac6da083b0d35..eec3b06e8dd46 100644 --- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx @@ -17,7 +17,7 @@ import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardTypeStep from './CardTypeStep'; import ConfirmationStep from './ConfirmationStep'; -import InviteNewMemberStep from './InviteeNewMemberStep'; +import InviteNewMemberStep from './InviteNewMemberStep'; import LimitStep from './LimitStep'; import LimitTypeStep from './LimitTypeStep'; diff --git a/src/types/onyx/AssignCard.ts b/src/types/onyx/AssignCard.ts index c497551bbf1fd..b344ac466fc6c 100644 --- a/src/types/onyx/AssignCard.ts +++ b/src/types/onyx/AssignCard.ts @@ -26,6 +26,9 @@ type AssignCardData = { /** An option based on which the transaction start date is chosen */ dateOption: string; + + /** The account ID of the cardholder */ + assigneeAccountID?: number; }; /** Model of assign card flow */ From e92ef2b92a91e4b2bd5c938bf5e0506cb1987e0c Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 27 May 2025 22:14:23 +0530 Subject: [PATCH 08/13] fix esLint --- .../companyCards/assignCard/AssignCardFeedPage.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 5f9c1fbd4b9b5..5633a0cfee248 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -1,13 +1,9 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useEffect} from 'react'; import {useOnyx} from 'react-native-onyx'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; -import {useBetas} from '@components/OnyxProvider'; -import {useOptionsList} from '@components/OptionListContextProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import useDebouncedState from '@hooks/useDebouncedState'; import useInitial from '@hooks/useInitial'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import BankConnection from '@pages/workspace/companyCards/BankConnection'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; From f8e948a3a26cd56d57be75b1aa4e5055963700d1 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 4 Jun 2025 08:08:45 +0000 Subject: [PATCH 09/13] fix: public domain user not displayed in list --- .../workspace/companyCards/assignCard/AssigneeStep.tsx | 8 +------- .../workspace/expensifyCard/issueNew/AssigneeStep.tsx | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index a084b371fa50f..1e19be0188b91 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -17,7 +17,6 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import {searchInServer} from '@libs/actions/Report'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; @@ -220,7 +219,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ]; } - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); const filteredOptions = tokenizedSearch(membersDetails, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); return [ @@ -241,10 +239,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ]; }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); - useEffect(() => { - searchInServer(debouncedSearchValue); - }, [debouncedSearchValue]); - return ( [option.text ?? '', option.alternateText ?? '']); return [ @@ -204,10 +202,6 @@ function AssigneeStep({policy}: AssigneeStepProps) { ]; }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); - useEffect(() => { - searchInServer(debouncedSearchValue); - }, [debouncedSearchValue]); - return ( Date: Tue, 17 Jun 2025 16:48:40 +0000 Subject: [PATCH 10/13] remove the step names from old file --- src/CONST.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ae8f63b325b64..8ae664811cb73 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3280,7 +3280,6 @@ const CONST = { STEP: { BANK_CONNECTION: 'BankConnection', ASSIGNEE: 'Assignee', - INVITE_NEW_MEMBER: 'InviteNewMember', CARD: 'Card', CARD_NAME: 'CardName', TRANSACTION_START_DATE: 'TransactionStartDate', @@ -3323,8 +3322,6 @@ const CONST = { STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', - INVITE_NEW_MEMBER: 'InviteNewMember', - CARD_TYPE: 'CardType', LIMIT_TYPE: 'LimitType', LIMIT: 'Limit', From 038112923ae114c955d1c7b1177a9a9e4dca1490 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:50:58 +0000 Subject: [PATCH 11/13] add back assignee step in CONST --- src/CONST/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 919f92afac61a..69830706dfec8 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3278,6 +3278,7 @@ const CONST = { BANK_CONNECTION: 'BankConnection', PLAID_CONNECTION: 'PlaidConnection', ASSIGNEE: 'Assignee', + INVITE_NEW_MEMBER: 'InviteNewMember', CARD: 'Card', CARD_NAME: 'CardName', TRANSACTION_START_DATE: 'TransactionStartDate', @@ -3320,6 +3321,7 @@ const CONST = { STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP: { ASSIGNEE: 'Assignee', + INVITE_NEW_MEMBER: 'InviteNewMember', CARD_TYPE: 'CardType', LIMIT_TYPE: 'LimitType', LIMIT: 'Limit', From 5db7ffb5291acea0d2055788c7a72f388d567adb Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 18 Jun 2025 00:50:13 +0530 Subject: [PATCH 12/13] fix results not shown for existing accounts --- .../companyCards/assignCard/AssigneeStep.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 1e19be0188b91..5659aabf1b473 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -17,6 +17,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {searchInServer} from '@libs/actions/Report'; import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; @@ -106,7 +107,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { const isEditing = assignCard?.isEditing; const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); - const {userToInvite, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); + const {userToInvite, personalDetails, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const [shouldShowError, setShouldShowError] = useState(false); const selectMember = (assignee: ListItem) => { @@ -236,8 +237,21 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, ] : []), + ...(personalDetails?.length > 0 + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: true, + }, + ] + : []), ]; - }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); + }, [debouncedSearchValue, membersDetails, personalDetails, searchValue, userToInvite]); + + useEffect(() => { + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); return ( Date: Wed, 18 Jun 2025 00:50:37 +0530 Subject: [PATCH 13/13] fix details not shown for existing users --- .../expensifyCard/issueNew/AssigneeStep.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index d154f3da27394..380542b1011d8 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -13,6 +13,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import {searchInServer} from '@libs/actions/Report'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import {filterAndOrderOptions, getHeaderMessage, getValidOptions, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail, getUserNameByEmail} from '@libs/PersonalDetailsUtils'; @@ -93,7 +94,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); - const {userToInvite, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); + const {userToInvite, personalDetails, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, headerMessage} = useOptions(); const isEditing = issueNewCard?.isEditing; const submit = (assignee: ListItem) => { @@ -199,8 +200,21 @@ function AssigneeStep({policy}: AssigneeStepProps) { }, ] : []), + ...(personalDetails?.length > 0 + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: true, + }, + ] + : []), ]; - }, [debouncedSearchValue, membersDetails, searchValue, userToInvite]); + }, [debouncedSearchValue, membersDetails, searchValue, userToInvite, personalDetails]); + + useEffect(() => { + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); return (