Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete expense invite in new expense flow #10959

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/LoginBtn.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class LoginBtn extends React.Component {
children: PropTypes.node,
loadingLoggedInUser: PropTypes.bool,
asLink: PropTypes.bool,
className: PropTypes.string,
};

static defaultProps = {
Expand Down Expand Up @@ -46,6 +47,7 @@ class LoginBtn extends React.Component {
this.props.asLink
? 'inline'
: 'inline-flex items-center justify-center rounded-full border text-sm whitespace-nowrap',
this.props.className,
)}
>
{this.props.loadingLoggedInUser ? (
Expand Down
5 changes: 4 additions & 1 deletion components/expenses/DeclineExpenseInviteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Textarea } from '../ui/Textarea';
type DeclineExpenseInviteButtonProps = {
expense: Pick<Expense, 'legacyId' | 'id'>;
draftKey?: string;
onExpenseInviteDeclined?: () => void;
};

const schema = z.object({
Expand All @@ -40,14 +41,16 @@ export default function DeclineExpenseInviteButton(props: DeclineExpenseInviteBu
}, [isDeclineModalOpen, resetForm]);

const { setFieldTouched, submitForm, validateForm } = formik;
const { onExpenseInviteDeclined } = props;
const onConfirm = React.useCallback(async () => {
const errors = await validateForm();
setFieldTouched('declineReason');
if (errors.declineReason) {
throw new Error(errors.declineReason);
}
await submitForm();
}, [submitForm, validateForm, setFieldTouched]);
onExpenseInviteDeclined?.();
}, [submitForm, validateForm, setFieldTouched, onExpenseInviteDeclined]);

return (
<React.Fragment>
Expand Down
73 changes: 68 additions & 5 deletions components/expenses/Expense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import useLoggedInUser from '../../lib/hooks/useLoggedInUser';
import { usePrevious } from '../../lib/hooks/usePrevious';
import { useWindowResize, VIEWPORTS } from '../../lib/hooks/useWindowResize';
import { itemHasOCR } from './lib/ocr';
import { PREVIEW_FEATURE_KEYS } from '@/lib/preview-features';

import ConfirmationModal from '../ConfirmationModal';
import Container from '../Container';
Expand All @@ -43,13 +44,15 @@ import MessageBox from '../MessageBox';
import StyledButton from '../StyledButton';
import StyledCheckbox from '../StyledCheckbox';
import StyledLink from '../StyledLink';
import { SubmitExpenseFlow } from '../submit-expense/SubmitExpenseFlow';
import { H1, H5, Span } from '../Text';

import { editExpenseMutation } from './graphql/mutations';
import { expensePageQuery } from './graphql/queries';
import { ConfirmOCRValues } from './ConfirmOCRValues';
import ExpenseForm, { msg as expenseFormMsg, prepareExpenseForSubmit } from './ExpenseForm';
import ExpenseInviteNotificationBanner from './ExpenseInviteNotificationBanner';
import ExpenseInviteWelcome, { ExpenseInviteRecipientNote } from './ExpenseInviteWelcome';
import ExpenseMissingReceiptNotificationBanner from './ExpenseMissingReceiptNotificationBanner';
import ExpenseNotesForm from './ExpenseNotesForm';
import ExpenseRecurringBanner from './ExpenseRecurringBanner';
Expand Down Expand Up @@ -122,9 +125,20 @@ function Expense(props) {
const { LoggedInUser, loadingLoggedInUser } = useLoggedInUser();
const intl = useIntl();
const router = useRouter();
const isNewExpenseSubmissionFlow =
(LoggedInUser && LoggedInUser.hasPreviewFeatureEnabled(PREVIEW_FEATURE_KEYS.NEW_EXPENSE_FLOW)) ||
router.query.newExpenseFlow;

const [isSubmittionFlowOpen, setIsSubmittionFlowOpen] = React.useState(false);

const [state, setState] = useState({
error: error || null,
status: draftKey && data?.expense?.status === ExpenseStatus.DRAFT ? PAGE_STATUS.EDIT : PAGE_STATUS.VIEW,
status:
draftKey && data?.expense?.status === ExpenseStatus.DRAFT
? isNewExpenseSubmissionFlow
? PAGE_STATUS.VIEW
: PAGE_STATUS.EDIT
: PAGE_STATUS.VIEW,
editedExpense: null,
isSubmitting: false,
isPollingEnabled: true,
Expand All @@ -146,7 +160,9 @@ function Expense(props) {
const drawerActionsContainer = useDrawerActionsContainer();

useEffect(() => {
const shouldEditDraft = data?.expense?.status === ExpenseStatus.DRAFT && draftKey;
const shouldEditDraft = isNewExpenseSubmissionFlow
? false
: data?.expense?.status === ExpenseStatus.DRAFT && draftKey;
if (shouldEditDraft) {
setState(state => ({
...state,
Expand Down Expand Up @@ -194,11 +210,16 @@ function Expense(props) {

// Update status when data or draftKey changes
useEffect(() => {
const status = draftKey && data?.expense?.status === ExpenseStatus.DRAFT ? PAGE_STATUS.EDIT : PAGE_STATUS.VIEW;
const status =
draftKey && data?.expense?.status === ExpenseStatus.DRAFT
? isNewExpenseSubmissionFlow
? PAGE_STATUS.VIEW
: PAGE_STATUS.EDIT
: PAGE_STATUS.VIEW;
if (status !== state.status) {
setState(state => ({ ...state, status }));
}
}, [props.data, draftKey]);
}, [props.data, draftKey, isNewExpenseSubmissionFlow]);

// Scroll to expense's top when changing status
const prevState = usePrevious(state);
Expand Down Expand Up @@ -582,14 +603,41 @@ function Expense(props) {
</MessageBox>
)}
{status === PAGE_STATUS.VIEW &&
((expense?.status === ExpenseStatus.UNVERIFIED && state.createdUser) || (isDraft && !isRecurring)) && (
((expense?.status === ExpenseStatus.UNVERIFIED && state.createdUser) ||
(isDraft && !isRecurring && !draftKey)) && (
<ExpenseInviteNotificationBanner expense={expense} createdUser={state.createdUser} />
)}
{isMissingReceipt && (
<ExpenseMissingReceiptNotificationBanner onEdit={status !== PAGE_STATUS.EDIT && onEditBtnClick} />
)}
{status !== PAGE_STATUS.EDIT && (
<Box mb={3}>
{isNewExpenseSubmissionFlow &&
(expense?.permissions?.canDeclineExpenseInvite ||
(expense?.status === ExpenseStatus.DRAFT &&
!isRecurring &&
draftKey &&
expense?.draft?.recipientNote)) && (
<React.Fragment>
<ExpenseInviteWelcome
onContinueSubmissionClick={() => {
setIsSubmittionFlowOpen(true);
}}
className="mb-4 rounded-md border border-[#DCDDE0] px-6 py-3"
expense={expense}
draftKey={draftKey}
/>
{expense?.draft?.recipientNote && (
<div className="mb-3 text-lg font-bold">
<FormattedMessage defaultMessage="Invitation note" id="WcjKTY" />
</div>
)}
<ExpenseInviteRecipientNote
className="mb-4 rounded-md border border-[#DCDDE0] px-6 py-3"
expense={expense}
/>
</React.Fragment>
)}
<ExpenseSummary
expense={status === PAGE_STATUS.EDIT_SUMMARY ? editedExpense : expense}
host={host}
Expand Down Expand Up @@ -796,6 +844,21 @@ function Expense(props) {
</Fragment>
)}

{isSubmittionFlowOpen && (
<SubmitExpenseFlow
onClose={submitted => {
setIsSubmittionFlowOpen(false);
if (submitted) {
refetch();
}
}}
expenseId={legacyExpenseId}
draftKey={draftKey}
submitExpenseTo={collective?.slug}
endFlowButtonLabel={<FormattedMessage defaultMessage="View expense" id="CaE5Oi" />}
/>
)}

{state.showFilesViewerModal &&
createPortal(
<FilesViewerModal
Expand Down
16 changes: 14 additions & 2 deletions components/expenses/ExpenseForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import ExpenseFormItems from './ExpenseFormItems';
import ExpenseFormPayeeInviteNewStep, { validateExpenseFormPayeeInviteNewStep } from './ExpenseFormPayeeInviteNewStep';
import ExpenseFormPayeeSignUpStep from './ExpenseFormPayeeSignUpStep';
import ExpenseFormPayeeStep, { checkStepOneCompleted } from './ExpenseFormPayeeStep';
import ExpenseInviteWelcome from './ExpenseInviteWelcome';
import ExpenseInviteWelcome, { ExpenseInviteRecipientNote } from './ExpenseInviteWelcome';
import { prepareExpenseItemForSubmit, validateExpenseItem } from './ExpenseItemForm';
import ExpenseRecurringBanner from './ExpenseRecurringBanner';
import ExpenseSummaryAdditionalInformation from './ExpenseSummaryAdditionalInformation';
Expand Down Expand Up @@ -635,7 +635,19 @@ const ExpenseFormBody = ({
<Form ref={formRef}>
{(expense?.permissions?.canDeclineExpenseInvite ||
(expense?.status === ExpenseStatus.DRAFT && expense?.draft?.recipientNote)) && (
<ExpenseInviteWelcome expense={expense} draftKey={router.query.key} />
<React.Fragment>
<ExpenseInviteWelcome
className="mb-8 rounded-md border border-[#DCDDE0] px-6 py-3"
expense={expense}
draftKey={router.query.key}
/>
{expense?.draft?.recipientNote && (
<div className="mb-3 text-lg font-bold">
<FormattedMessage defaultMessage="Invitation note" id="WcjKTY" />
</div>
)}
<ExpenseInviteRecipientNote className="mb-4 rounded-md border border-[#DCDDE0] px-6 py-3" expense={expense} />
</React.Fragment>
)}
{!isCreditCardCharge && (
<ExpenseTypeRadioSelect
Expand Down
144 changes: 88 additions & 56 deletions components/expenses/ExpenseInviteWelcome.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import clsx from 'clsx';
import { FormattedMessage } from 'react-intl';

import type { AccountHoverCardFieldsFragment } from '../../lib/graphql/types/v2/graphql';
Expand All @@ -7,15 +8,17 @@ import { AccountHoverCard } from '../AccountHoverCard';
import Avatar from '../Avatar';
import DateTime from '../DateTime';
import Image from '../Image';
import { Button } from '../ui/Button';

import DeclineExpenseInviteButton from './DeclineExpenseInviteButton';

type ExpenseInviteWelcomeProps = {
className?: string;
expense: {
id: string;
legacyId: number;
createdAt: Date;
createdByAccount: AccountHoverCardFieldsFragment;
createdByAccount?: AccountHoverCardFieldsFragment;
draft?: {
recipientNote?: string;
};
Expand All @@ -24,71 +27,100 @@ type ExpenseInviteWelcomeProps = {
};
};
draftKey?: string;
onExpenseInviteDeclined?: () => void;
onContinueSubmissionClick?: () => void;
};

export default function ExpenseInviteWelcome(props: ExpenseInviteWelcomeProps) {
return (
<React.Fragment>
<div className="mb-8 items-center gap-4 rounded-md border border-[#DCDDE0] px-6 py-3 md:flex">
<Image
className="hidden object-contain md:block"
alt=""
src="/static/images/pidgeon.png"
width={132}
height={132}
/>
<div className="grow">
<div className="mb-2 text-lg font-bold">
<FormattedMessage defaultMessage="You have been invited to submit an expense" id="wyn788" />
</div>
<div className="mb-2 text-sm">
<FormattedMessage
defaultMessage="Some details were pre-filled for you by the person who invited you. Make sure you double check all the information that was pre-filled and correct it if necessary. If you have any questions contact this person directly off-platform."
id="5YI3tk"
/>
</div>
{props.expense.permissions.canDeclineExpenseInvite && (
<div className="mt-2">
<DeclineExpenseInviteButton expense={props.expense} draftKey={props.draftKey} />
</div>
)}
<div className={clsx('items-center gap-4 md:flex', props.className)}>
<Image
className="hidden object-contain md:block"
alt=""
src="/static/images/pidgeon.png"
width={132}
height={132}
/>
<div className="grow">
<div className="mb-2 text-lg font-bold">
<FormattedMessage defaultMessage="You have been invited to submit an expense" id="wyn788" />
</div>
<div className="mb-2 text-sm">
<FormattedMessage
defaultMessage="Some details were pre-filled for you by the person who invited you. Make sure you double check all the information that was pre-filled and correct it if necessary. If you have any questions contact this person directly off-platform."
id="5YI3tk"
/>
</div>
{(props.expense.permissions.canDeclineExpenseInvite || props.onContinueSubmissionClick) && (
<div className="mt-2 flex gap-2">
{props.expense.permissions.canDeclineExpenseInvite && (
<div>
<DeclineExpenseInviteButton
onExpenseInviteDeclined={props.onExpenseInviteDeclined}
expense={props.expense}
draftKey={props.draftKey}
/>
</div>
)}
{props.onContinueSubmissionClick && (
<Button size="xs" onClick={() => props.onContinueSubmissionClick()}>
<FormattedMessage defaultMessage="Continue submission" id="rfhXwf" />
</Button>
)}
</div>
)}
</div>
</div>
);
}

{props.expense.draft?.recipientNote && (
<div className="mb-4 rounded-md border border-[#DCDDE0] px-6 py-3">
<div className="mb-3 text-lg font-bold">
<FormattedMessage defaultMessage="Invitation note" id="WcjKTY" />
</div>
type ExpenseInviteRecipientNoteProps = {
className?: string;
expense: {
id: string;
legacyId: number;
createdAt: Date;
createdByAccount?: AccountHoverCardFieldsFragment;
draft?: {
recipientNote?: string;
};
permissions: {
canDeclineExpenseInvite?: boolean;
};
};
};

<div className="mb-3">
<AccountHoverCard
account={props.expense.createdByAccount}
trigger={
<div className="flex items-center gap-2 truncate">
<Avatar collective={props.expense.createdByAccount} radius={24} />
<div>
<div className="text-sm font-medium">
<FormattedMessage
defaultMessage="By {userName}"
id="ByUser"
values={{
userName: props.expense.createdByAccount.name,
}}
/>
</div>
<div className="text-xs text-muted-foreground">
<DateTime value={props.expense.createdAt} />
</div>
export function ExpenseInviteRecipientNote(props: ExpenseInviteRecipientNoteProps) {
return (
props.expense.draft?.recipientNote && (
<div className={props.className}>
<div className="mb-3">
<AccountHoverCard
account={props.expense.createdByAccount}
trigger={
<div className="flex items-center gap-2 truncate">
<Avatar collective={props.expense.createdByAccount} radius={24} />
<div>
<div className="text-sm font-medium">
<FormattedMessage
defaultMessage="By {userName}"
id="ByUser"
values={{
userName: props.expense.createdByAccount.name,
}}
/>
</div>
<div className="text-xs text-muted-foreground">
<DateTime value={props.expense.createdAt} />
</div>
</div>
}
/>
</div>

<div className="text-sm">{props.expense.draft.recipientNote}</div>
</div>
}
/>
</div>
)}
</React.Fragment>

<div className="text-sm">{props.expense.draft.recipientNote}</div>
</div>
)
);
}
Loading
Loading