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', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7b8702a494ad8..5c8a275582de0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -994,7 +994,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 bfca86d70ef54..63a09bee43440 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4826,6 +4826,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 27e49bf27fd10..6e83b9699438a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4873,6 +4873,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/Member.ts b/src/libs/actions/Policy/Member.ts index fc4c83cb92b5c..8b94d7a33373f 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -967,7 +967,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, @@ -1165,7 +1170,7 @@ function setWorkspaceInviteRoleDraft(policyID: string, role: ValueOf & WithPolicyAndFullscreenLoadingProps; @@ -68,6 +69,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(); @@ -50,13 +107,23 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { const isEditing = assignCard?.isEditing; const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {userToInvite, personalDetails, 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 = () => { @@ -144,7 +211,7 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { }, [isOffline, policy?.employeeList, selectedMember]); const sections = useMemo(() => { - if (!debouncedSearchTerm) { + if (!debouncedSearchValue) { return [ { data: membersDetails, @@ -153,7 +220,6 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { ]; } - const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); const filteredOptions = tokenizedSearch(membersDetails, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); return [ @@ -162,14 +228,30 @@ function AssigneeStep({policy, feed}: AssigneeStepProps) { data: filteredOptions, shouldShow: true, }, + ...(userToInvite + ? [ + { + title: undefined, + data: [userToInvite], + shouldShow: true, + }, + ] + : []), + ...(personalDetails?.length > 0 + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: true, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); + }, [debouncedSearchValue, membersDetails, personalDetails, 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/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx index c911b60ca004c..380542b1011d8 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 tokenizedSearch from '@libs/tokenizedSearch'; @@ -30,6 +33,60 @@ 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 defaultOptions = useMemo(() => { + 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}: AssigneeStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -37,15 +94,24 @@ function AssigneeStep({policy}: AssigneeStepProps) { const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {canBeMissing: true}); + const {userToInvite, personalDetails, 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')); @@ -106,16 +172,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 = tokenizedSearch(membersDetails, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); return [ @@ -123,15 +189,32 @@ function AssigneeStep({policy}: AssigneeStepProps) { title: undefined, data: filteredOptions, shouldShow: true, + isDisabled: undefined, }, + ...(userToInvite + ? [ + { + title: undefined, + data: [userToInvite], + shouldShow: true, + }, + ] + : []), + ...(personalDetails?.length > 0 + ? [ + { + title: undefined, + data: personalDetails, + shouldShow: true, + }, + ] + : []), ]; - }, [membersDetails, debouncedSearchTerm]); - - const headerMessage = useMemo(() => { - const searchValue = debouncedSearchTerm.trim().toLowerCase(); + }, [debouncedSearchValue, membersDetails, searchValue, userToInvite, personalDetails]); - 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}`, {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 = () => { + 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 ?? CONST.DEFAULT_NUMBER_ID, + }, + `${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..eec3b06e8dd46 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 './InviteNewMemberStep'; 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/AssignCard.ts b/src/types/onyx/AssignCard.ts index dd0ba64f86831..5b220a4899656 100644 --- a/src/types/onyx/AssignCard.ts +++ b/src/types/onyx/AssignCard.ts @@ -29,6 +29,9 @@ type AssignCardData = { /** An option based on which the transaction start date is chosen */ dateOption: string; + /** The account ID of the cardholder */ + assigneeAccountID?: number; + /** bank id for Plaid */ institutionId?: string; 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;