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/17] 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 b380d1c9e153..b646bb061904 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/17] 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 871450f669d0..e27146d9e0eb 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 526a16097e2e..7b2421a29f00 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 bf25be5bd770..2d1b184370a8 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 0e48eee26d1b..c45ce8b09338 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 0cd2b8155c8a..2f6137ed806f 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 e8978af095c6..99c592c89a21 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 e845e3cf22bc..ac6da083b0d3 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 1ddcd51a9f68..784bcb72cd59 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/17] 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 384b8530ed3f..0dadbf810342 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 6869db9feb6d..be1fc6f0c859 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/17] 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 be1fc6f0c859..8a94e54d993b 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/17] 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 8a94e54d993b..8d3f7d748b14 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/17] 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 85004f8a4562..d147406c6490 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 cd6d136dc2c7..02e5dcf9fe73 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/17] 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 e27146d9e0eb..38215c1f2314 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 234c6000d115..5f9c1fbd4b9b 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 000000000000..f22a35005ec1 --- /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 ac6da083b0d3..eec3b06e8dd4 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 c497551bbf1f..b344ac466fc6 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/17] 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 5f9c1fbd4b9b..5633a0cfee24 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/17] 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 a084b371fa50..1e19be0188b9 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/17] 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 ae8f63b325b6..8ae664811cb7 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/17] 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 919f92afac61..69830706dfec 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/17] 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 1e19be0188b9..5659aabf1b47 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/17] 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 d154f3da2739..380542b1011d 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 ( Date: Tue, 17 Jun 2025 19:40:54 +0000 Subject: [PATCH 14/17] add translations --- src/languages/de.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 8 files changed, 8 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index d81832236d38..ccc3c95b9194 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4921,6 +4921,7 @@ const translations = { invite: { member: 'Mitglied einladen', members: 'Mitglieder einladen', + inviteNewMember: 'Neues Mitglied einladen', invitePeople: 'Neue Mitglieder einladen', genericFailureMessage: 'Beim Einladen des Mitglieds in den Workspace ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.', pleaseEnterValidLogin: `Bitte stellen Sie sicher, dass die E-Mail-Adresse oder Telefonnummer g\u00FCltig ist (z. B. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0a7d1def6304..c0456aa7b433 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4950,6 +4950,7 @@ const translations = { invite: { member: 'Inviter un membre', members: 'Inviter des membres', + inviteNewMember: 'Inviter un nouveau membre', invitePeople: 'Inviter de nouveaux membres', genericFailureMessage: "Une erreur s'est produite lors de l'invitation du membre \u00E0 l'espace de travail. Veuillez r\u00E9essayer.", pleaseEnterValidLogin: `Veuillez vous assurer que l'email ou le num\u00E9ro de t\u00E9l\u00E9phone est valide (par exemple, ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 8b7f37ba54ff..4f3bde8c47c8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4885,6 +4885,7 @@ const translations = { invite: { member: 'Invita membro', members: 'Invita membri', + inviteNewMember: 'Invita nuovo membro', invitePeople: 'Invita nuovi membri', genericFailureMessage: "Si \u00E8 verificato un errore durante l'invito del membro allo spazio di lavoro. Per favore riprova.", pleaseEnterValidLogin: `Assicurati che l'email o il numero di telefono siano validi (ad es. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f1487c2b2b98..d2499ba71426 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5457,6 +5457,7 @@ const translations = { invite: { member: '\u30E1\u30F3\u30D0\u30FC\u3092\u62DB\u5F85\u3059\u308B', members: '\u30E1\u30F3\u30D0\u30FC\u3092\u62DB\u5F85\u3059\u308B', + inviteNewMember: '新しいメンバーを招待する', invitePeople: '\u65B0\u3057\u3044\u30E1\u30F3\u30D0\u30FC\u3092\u62DB\u5F85\u3059\u308B', genericFailureMessage: '\u30EF\u30FC\u30AF\u30B9\u30DA\u30FC\u30B9\u306B\u30E1\u30F3\u30D0\u30FC\u3092\u62DB\u5F85\u3059\u308B\u969B\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 15eb1ec1f5ed..1e03eebdf7b9 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4869,6 +4869,7 @@ const translations = { invite: { member: 'Lid uitnodigen', members: 'Leden uitnodigen', + inviteNewMember: 'Nieuw lid uitnodigen', invitePeople: 'Nieuwe leden uitnodigen', genericFailureMessage: 'Er is een fout opgetreden bij het uitnodigen van het lid voor de werkruimte. Probeer het opnieuw.', pleaseEnterValidLogin: `Zorg ervoor dat het e-mailadres of telefoonnummer geldig is (bijv. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 56c729a50614..1fbe795daa1e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4953,6 +4953,7 @@ const translations = { invite: { member: 'Zapro\u015B cz\u0142onka', members: 'Zapro\u015B cz\u0142onk\u00F3w', + inviteNewMember: 'Zaproś nowego członka', invitePeople: 'Zapro\u015B nowych cz\u0142onk\u00F3w', genericFailureMessage: 'Wyst\u0105pi\u0142 b\u0142\u0105d podczas zapraszania cz\u0142onka do przestrzeni roboczej. Prosz\u0119 spr\u00F3bowa\u0107 ponownie.', pleaseEnterValidLogin: `Prosz\u0119 upewni\u0107 si\u0119, \u017Ce adres e-mail lub numer telefonu jest prawid\u0142owy (np. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 8b77ef5b2768..9d0df1f05b8e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4914,6 +4914,7 @@ const translations = { invite: { member: 'Convidar membro', members: 'Convidar membros', + inviteNewMember: 'Convidar novo membro', invitePeople: 'Convidar novos membros', genericFailureMessage: 'Ocorreu um erro ao convidar o membro para o espa\u00E7o de trabalho. Por favor, tente novamente.', pleaseEnterValidLogin: `Por favor, certifique-se de que o e-mail ou n\u00FAmero de telefone \u00E9 v\u00E1lido (por exemplo, ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d37a54c42ba4..5e70f1f4da54 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5069,6 +5069,7 @@ const translations = { invite: { member: '\u9080\u8BF7\u6210\u5458', members: '\u9080\u8BF7\u6210\u5458', + inviteNewMember: '邀请新成员', invitePeople: '\u9080\u8BF7\u65B0\u6210\u5458', genericFailureMessage: '\u9080\u8BF7\u6210\u5458\u52A0\u5165\u5DE5\u4F5C\u533A\u65F6\u53D1\u751F\u9519\u8BEF\u3002\u8BF7\u518D\u8BD5\u4E00\u6B21\u3002', pleaseEnterValidLogin: `\u8BF7\u786E\u4FDD\u7535\u5B50\u90AE\u4EF6\u6216\u7535\u8BDD\u53F7\u7801\u6709\u6548\uFF08\u4F8B\u5982\uFF1A${CONST.EXAMPLE_PHONE_NUMBER}\uFF09\u3002`, From a960cc4e1caa4451237d0b613aee623e4d12e7b4 Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 18 Jun 2025 22:04:36 +0530 Subject: [PATCH 15/17] fix prettier --- src/languages/ja.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ac58211c13bd..9cd7ed2d4f81 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4823,7 +4823,7 @@ const translations = { invite: { member: 'メンバーを招待', members: 'メンバーを招待する', - inviteNewMember: '新しいメンバーを招待する', + inviteNewMember: '新しいメンバーを招待する', invitePeople: '新しいメンバーを招待する', genericFailureMessage: 'メンバーをワークスペースに招待する際にエラーが発生しました。もう一度お試しください。', pleaseEnterValidLogin: `メールアドレスまたは電話番号が有効であることを確認してください(例: ${CONST.EXAMPLE_PHONE_NUMBER})。`, From 0ef021ae7c21703215f502082aadec61c428727c Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:21:20 +0000 Subject: [PATCH 16/17] make code dry --- .../InviteNewMemberStepComponent.tsx | 199 ++++++++++++++++++ .../assignCard/InviteNewMemberStep.tsx | 167 +-------------- .../issueNew/InviteNewMemberStep.tsx | 176 +--------------- 3 files changed, 221 insertions(+), 321 deletions(-) create mode 100644 src/components/InviteNewMemberStepComponent.tsx diff --git a/src/components/InviteNewMemberStepComponent.tsx b/src/components/InviteNewMemberStepComponent.tsx new file mode 100644 index 000000000000..89d28de12a1c --- /dev/null +++ b/src/components/InviteNewMemberStepComponent.tsx @@ -0,0 +1,199 @@ +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 useAutoFocusInput from '@hooks/useAutoFocusInput'; +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 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 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 {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import FormProvider from './Form/FormProvider'; +import InputWrapper from './Form/InputWrapper'; +import InteractiveStepWrapper from './InteractiveStepWrapper'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import MultipleAvatars from './MultipleAvatars'; +import type {AnimatedTextInputRef} from './RNTextInput'; +import Text from './Text'; +import TextInput from './TextInput'; + +type InviteNewMemberStepComponentProps = { + // The title of the header + title: string; + + // The function to call when the back button is pressed + handleBackButtonPress: () => void; + + // Whether the user is editing step + isEditing?: boolean; + + // The email address of the assignee + assigneeEmail: string | undefined; + + // The name of the step + stepNames: string[]; + + // The account ID of the cardholder + assigneeAccountID: number | undefined; + + // The policy that the card will be issued under + policy: OnyxEntry; + + // The function to call when the user wants to go to the next step + goToNextStep: () => void; +}; +function InviteNewMemberStepComponent({title, handleBackButtonPress, isEditing, assigneeEmail, assigneeAccountID, policy, goToNextStep, stepNames}: InviteNewMemberStepComponentProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policyID = policy?.id; + const [welcomeNote, setWelcomeNote] = useState(); + const {inputCallbackRef, inputRef} = useAutoFocusInput(); + const [workspaceInviteRoleDraft = CONST.POLICY.ROLE.USER] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_ROLE_DRAFT}${policyID}`, {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 currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); + 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(assigneeEmail)) { + setWelcomeNote(getDefaultWelcomeNote()); + } + }, [assigneeEmail, getDefaultWelcomeNote, isOnyxLoading]); + + 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( + { + [assigneeEmail ?? '']: assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID, + }, + `${welcomeNoteSubject}\n\n${welcomeNote}`, + policyID, + policyMemberAccountIDs, + workspaceInviteRoleDraft, + ); + setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); + clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); + + goToNextStep(); + }; + + useEffect(() => { + return () => { + clearWorkspaceInviteRoleDraft(policyID); + }; + }, [policyID]); + + return ( + + {translate('workspace.invite.inviteNewMember')} + + + + + + {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 + /> + + + + ); +} + +InviteNewMemberStepComponent.displayName = 'InviteNewMemberStepComponent'; + +export default InviteNewMemberStepComponent; diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx index f22a35005ec1..7b5091745a63 100644 --- a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -1,39 +1,16 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +import React from 'react'; 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 InviteNewMemberStepComponent from '@components/InviteNewMemberStepComponent'; 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; @@ -42,49 +19,13 @@ type InviteNewMemberStepProps = { 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 = () => { @@ -103,23 +44,7 @@ function InviteNewMemberStep({policy, feed}: InviteNewMemberStepProps) { } }; - 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); - + const goToNextStep = () => { let nextStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; const data: Partial = { email: assignCard?.data?.email, @@ -139,86 +64,16 @@ function InviteNewMemberStep({policy, feed}: InviteNewMemberStepProps) { }); }; - 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 - /> - - - + stepNames={[...CONST.COMPANY_CARD.STEP_NAMES]} + assigneeEmail={assignCard?.data?.email} + assigneeAccountID={assignCard?.data?.assigneeAccountID} + policy={policy} + goToNextStep={goToNextStep} + /> ); } diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx index 8d3f7d748b14..14c1996c623f 100644 --- a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx @@ -1,36 +1,12 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +import React from 'react'; 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 type {FormInputErrors} from '@components/Form/types'; -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 useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import InviteNewMemberStepComponent from '@components/InviteNewMemberStepComponent'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; import {setIssueNewCardStepAndData} from '@libs/actions/Card'; -import {clearDraftValues} from '@libs/actions/FormActions'; -import {addMembersToWorkspace, clearWorkspaceInviteRoleDraft} from '@libs/actions/Policy/Member'; -import {setWorkspaceInviteMessageDraft} from '@libs/actions/Policy/Policy'; -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 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 {isEmptyObject} from '@src/types/utils/EmptyObject'; -import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; type InviteeNewMemberStepProps = { // The policy that the card will be issued under @@ -39,48 +15,8 @@ type InviteeNewMemberStepProps = { 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}`, {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(); - 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 = () => { @@ -97,22 +33,7 @@ function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { }); }; - 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 ?? CONST.DEFAULT_NUMBER_ID, - }, - `${welcomeNoteSubject}\n\n${welcomeNote}`, - policyID, - policyMemberAccountIDs, - workspaceInviteRoleDraft, - ); - setWorkspaceInviteMessageDraft(policyID, welcomeNote ?? null); - clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); - + const goToNextStep = () => { if (isEditing) { setIssueNewCardStepAndData({step: CONST.EXPENSIFY_CARD.STEP.CONFIRMATION, isEditing: false, policyID}); } else { @@ -120,91 +41,16 @@ function InviteNewMemberStep({policy}: InviteeNewMemberStepProps) { } }; - 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 - /> - - - + stepNames={[...CONST.EXPENSIFY_CARD.STEP_NAMES]} + assigneeEmail={issueNewCard?.data?.assigneeEmail} + assigneeAccountID={issueNewCard?.data?.assigneeAccountID} + policy={policy} + goToNextStep={goToNextStep} + /> ); } From d0009b096bfbf3567d84bff82820e39fa9e2999b Mon Sep 17 00:00:00 2001 From: Rutika Pawar <183392827+twilight2294@users.noreply.github.com> Date: Thu, 19 Jun 2025 05:16:02 +0000 Subject: [PATCH 17/17] show searchbar if members exist --- src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx | 4 +--- src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 5659aabf1b47..e459b6a429b5 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -31,8 +31,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; -const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; - type AssigneeStepProps = { /** The policy that the card will be issued under */ policy: OnyxEntry; @@ -172,7 +170,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { Navigation.goBack(); }; - const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const shouldShowSearchInput = policy?.employeeList; const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; const membersDetails = useMemo(() => { diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index 380542b1011d..9bb4536f35b3 100644 --- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx @@ -26,8 +26,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {IssueNewCardData} from '@src/types/onyx/Card'; -const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; - type AssigneeStepProps = { // The policy that the card will be issued under policy: OnyxEntry; @@ -134,7 +132,7 @@ function AssigneeStep({policy}: AssigneeStepProps) { clearIssueNewCardFlow(policyID); }; - const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const shouldShowSearchInput = policy?.employeeList; const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; const membersDetails = useMemo(() => {