Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | 1x 1x 1x 40x 1x 67x 67x 67x 67x 67x 67x 67x 27x 67x 7x 3x 2x 1x 3x 3x 3x 4x 67x | import { useEffect } from "react";
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import { Stack } from "@mui/material";
import { FormProvider, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";
import { useQueryClient } from "react-query";
import GiveBaseModal from "@shared/modals/GiveBaseModal";
import GiveButton from "@shared/Button/GiveButton";
import GiveText from "@shared/Text/GiveText";
import { HFGiveInput } from "@shared/HFInputs/HFGiveInput/HFGiveInput";
import { showMessage } from "@common/Toast";
import { QKEY_EVENT_HOSTS, QKEY_LIST_TEAM_MEMBERS } from "@constants/queryKeys";
import { useGetCurrentMerchantId } from "@hooks/common";
import { useInviteEventHost } from "@services/api/products/hosts";
import { isValidEmail } from "@validation/regex";
export const INVITE_HOST_HELPER =
"An invitation will be sent to this email address, and the host will need to accept it and log in to the GiveCash mobile app.";
/** Frame 7117-39026 draws no label above the field, so this names it for AT. */
export const INVITE_HOST_FIELD_LABEL = "Host email address";
type Props = {
eventId?: string;
};
type FormValues = { email: string };
const schema = Yup.object().shape({
email: Yup.string()
.required("Please enter an email.")
.test("valid-email", "Please enter a valid email.", (value) =>
isValidEmail(value || ""),
),
});
/**
* "Invite Host" (frames 7117-39026 / 7117-39652).
*
* One email per invitation — the annotation on 7117-39652 is explicit, so this
* is a plain field rather than the tag input the team-member modal uses.
*/
const InviteHostModal = NiceModal.create(({ eventId }: Props) => {
const modal = useModal();
const queryClient = useQueryClient();
const { merchantId } = useGetCurrentMerchantId();
const { mutate: invite, isLoading } = useInviteEventHost(merchantId, eventId);
const methods = useForm<FormValues>({
resolver: yupResolver(schema),
defaultValues: { email: "" },
mode: "onChange",
});
const {
formState: { isValid },
handleSubmit,
setError,
reset,
} = methods;
// NiceModal keeps the component mounted across hide/show, so without this the
// previous attempt's email (and its field error) greets the next open.
useEffect(() => {
if (modal.visible) reset();
}, [modal.visible, reset]);
const onSubmit = ({ email }: FormValues) =>
invite(email.trim(), {
onSuccess: ({ invitationSent }) => {
// An address that had already joined another merchant is seated
// straight away, with no invitation to accept — saying one was sent
// would send the merchant looking for an email that does not exist.
if (invitationSent) {
showMessage("Success", "", true, "Invitation Sent");
} else {
showMessage(
"Success",
"They already have a GiveCash account, so no invitation was needed.",
true,
"Host Added",
);
}
queryClient.invalidateQueries([QKEY_EVENT_HOSTS]);
// A host is an account member, so Settings → Team gained a row too.
queryClient.invalidateQueries(QKEY_LIST_TEAM_MEMBERS);
modal.hide();
},
// Surfaced on the field rather than as a toast: the most common failure
// is an address that is already a member of this merchant (409), which
// is about the value the merchant just typed. Field-level rejections
// arrive in the `input` envelope, non-field ones in `message` — the same
// pair `useInviteTeamMember` reads.
onError: (error: any) =>
setError("email", {
type: "manual",
message:
error?.response?.data?.input?.[0]?.message ||
error?.response?.data?.message ||
error?.message ||
"Could not send the invitation.",
}),
});
return (
<GiveBaseModal
open={modal.visible}
title="Invite Host"
width="600px"
height="fit-content"
onClose={modal.hide}
// `hide` only closes it; without the unmount on exit the next open still
// holds the previous email and its error.
TransitionProps={{ onExited: () => modal.remove() }}
buttons={
<Stack direction="row" gap="12px">
<GiveButton
variant="ghost"
size="large"
label="Cancel"
onClick={modal.hide}
/>
<GiveButton
size="large"
variant="filled"
color="primary"
label="Invite"
onClick={handleSubmit(onSubmit)}
disabled={!isValid || isLoading}
/>
</Stack>
}
>
<FormProvider {...methods}>
<Stack gap="8px">
{/* `disabled` is passed explicitly because HFGiveInput hard-sets it
on the underlying GiveInput and only the caller's value overrides
it — an omitted prop renders the field read-only.
The mockup has no visible label, and an empty `label` renders no
FormLabel at all, so the accessible name comes from aria-label. */}
<HFGiveInput
name="email"
placeholder="Enter email"
type="email"
disabled={false}
autoFocus
inputProps={{ "aria-label": INVITE_HOST_FIELD_LABEL }}
/>
{/* Deliberately its own line rather than the input's `helper` prop:
HFGiveInput resolves helperText as `helper || error?.message`, so
a permanent helper would swallow every validation and API error. */}
<GiveText variant="bodyXS" color="secondary">
{INVITE_HOST_HELPER}
</GiveText>
</Stack>
</FormProvider>
</GiveBaseModal>
);
});
export default InviteHostModal;
|