Feat - invite user to team flow improvements (#6725)

This commit is contained in:
sean-brydon 2023-01-31 18:16:43 +00:00 committed by GitHub
parent 99e71054ff
commit 208fc2f0e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 141 additions and 39 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 193 B

View File

@ -1,5 +1,3 @@
<svg stroke="#888888" fill="none" stroke-width="2.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33333 6H10.6667M10.6667 6L6 1.33333M10.6667 6L6 10.6667" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 360 B

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

@ -7,6 +7,7 @@
"upgrade_now": "Upgrade now",
"accept_invitation": "Accept Invitation",
"calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.",
"calcom_explained_new_user": "Finish setting up your {{appName}} account! Youre just a few steps away from solving all your scheduling problems.",
"have_any_questions": "Have questions? We're here to help.",
"reset_password_subject": "{{appName}}: Reset password instructions",
"event_declined_subject": "Declined: {{title}} at {{date}}",
@ -1536,5 +1537,15 @@
"delete_sso_configuration": "Delete {{connectionType}} configuration",
"delete_sso_configuration_confirmation": "Yes, delete {{connectionType}} configuration",
"delete_sso_configuration_confirmation_description": "Are you sure you want to delete the {{connectionType}} configuration? Your team members who use {{connectionType}} login will no longer be able to access Cal.com.",
"email_no_user_cta":"Create your account",
"email_user_cta":"View Invitation",
"email_no_user_invite_heading":"Youve been invited to join a team on {{appName}}",
"email_no_user_invite_subheading":"{{invitedBy}} has invited you to join their team on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.",
"email_no_user_invite_steps_intro":"Well walk you through a few short steps and youll be enjoying stress free scheduling with your team in no time.",
"email_no_user_step_one":"Choose your username",
"email_no_user_step_two":"Connect your calendar account",
"email_no_user_step_three":"Set your Availability",
"email_no_user_step_four":"Join {{teamName}}",
"email_no_user_signoff":"Happy Scheduling from the {{appName}} team",
"impersonation_user_tip": "You are about to impersonate a user, which means you can make changes on their behalf. Please be careful."
}

View File

@ -14,7 +14,7 @@ export const CallToAction = (props: { label: string; href: string; secondary?: b
margin: 0,
textDecoration: "none",
textTransform: "none",
padding: "10px 16px",
padding: "16px 24px",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
msoPaddingAlt: "0px",

View File

@ -26,8 +26,8 @@ export const V2BaseEmailHtml = (props: {
return (
<Html>
<EmailHead title={props.subject} />
<body style={{ wordSpacing: "normal", backgroundColor: "#F5F5F5" }}>
<div style={{ backgroundColor: "#F5F5F5" }}>
<body style={{ wordSpacing: "normal", backgroundColor: "#F3F4F6" }}>
<div style={{ backgroundColor: "#F3F4F6" }}>
<RawHtml
html={`<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->`}
/>

View File

@ -1,6 +1,7 @@
import type { TFunction } from "next-i18next";
import { ReactNode } from "react";
import { APP_NAME, BASE_URL, IS_PRODUCTION } from "@calcom/lib/constants";
import { APP_NAME, WEBAPP_URL, IS_PRODUCTION } from "@calcom/lib/constants";
import { V2BaseEmailHtml, CallToAction } from "../components";
@ -10,6 +11,7 @@ type TeamInvite = {
to: string;
teamName: string;
joinLink: string;
isCalcomMember: boolean;
};
export const TeamInviteEmail = (
@ -22,39 +24,100 @@ export const TeamInviteEmail = (
team: props.teamName,
appName: APP_NAME,
})}>
<p style={{ fontSize: "24px", marginBottom: "16px", textAlign: "center" }}>
<>{props.language("email_no_user_invite_heading", { appName: APP_NAME })}</>
</p>
<img
height="64"
style={{
borderRadius: "16px",
height: "270px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
src={
IS_PRODUCTION
? BASE_URL + "/emails/teamCircle@2x.png"
: "http://localhost:3000/emails/teamCircle@2x.png"
? WEBAPP_URL + "/emails/calendar-email-hero.png"
: "http://localhost:3000/emails/calendar-email-hero.png"
}
style={{
border: "0",
display: "block",
outline: "none",
textDecoration: "none",
height: "64px",
fontSize: "13px",
marginBottom: "24px",
}}
width="64"
alt=""
/>
<p style={{ fontSize: "24px", marginBottom: "16px" }}>
<>
{props.language("user_invited_you", {
user: props.from,
team: props.teamName,
appName: APP_NAME,
})}
!
</>
<p
style={{
fontWeight: 400,
lineHeight: "24px",
marginBottom: "32px",
marginTop: "32px",
lineHeightStep: "24px",
}}>
<>{props.language("email_no_user_invite_subheading", { invitedBy: props.from, appName: APP_NAME })}</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px", marginBottom: "32px" }}>
<>{props.language("calcom_explained", { appName: APP_NAME })}</>
<div style={{ display: "flex", justifyContent: "center" }}>
<CallToAction
label={props.language(props.isCalcomMember ? "email_user_cta" : "email_no_user_cta")}
href={props.joinLink}
/>
</div>
<p
style={{
fontWeight: 400,
lineHeight: "24px",
marginBottom: "32px",
marginTop: "48px",
lineHeightStep: "24px",
}}>
<>{props.language("email_no_user_invite_steps_intro")}</>
</p>
<CallToAction label={props.language("accept_invitation")} href={props.joinLink} />
{!props.isCalcomMember && (
<div style={{ display: "flex", flexDirection: "column", gap: "32px" }}>
<EmailStep
translationString={props.language("email_no_user_step_one")}
iconsrc={
IS_PRODUCTION
? WEBAPP_URL + "/emails/choose-username@2x.png"
: "http://localhost:3000/emails/choose-username@2x.png"
}
/>
<EmailStep
translationString={props.language("email_no_user_step_two")}
iconsrc={
IS_PRODUCTION
? WEBAPP_URL + "/emails/calendar@2x.png"
: "http://localhost:3000/emails/calendar@2x.png"
}
/>
<EmailStep
translationString={props.language("email_no_user_step_three")}
iconsrc={
IS_PRODUCTION
? WEBAPP_URL + "/emails/clock@2x.png"
: "http://localhost:3000/emails/clock@2x.png"
}
/>
<EmailStep
translationString={props.language("email_no_user_step_four", { teamName: props.teamName })}
iconsrc={
IS_PRODUCTION
? WEBAPP_URL + "/emails/user-check@2x.png"
: "http://localhost:3000/emails/user-check@2x.png"
}
/>
</div>
)}
<div className="">
<p
style={{
fontWeight: 400,
lineHeight: "24px",
marginBottom: "32px",
marginTop: "32px",
lineHeightStep: "24px",
}}>
<>{props.language("email_no_user_signoff", { appName: APP_NAME })}</>
</p>
</div>
<div style={{ borderTop: "1px solid #E1E1E1", marginTop: "32px", paddingTop: "32px" }}>
<p style={{ fontWeight: 400, margin: 0 }}>
@ -70,3 +133,34 @@ export const TeamInviteEmail = (
</V2BaseEmailHtml>
);
};
const EmailStep = (props: { translationString: string; iconsrc: string }) => {
return (
<div style={{ display: "flex", alignItems: "center" }}>
<div
style={{
backgroundColor: "#E5E7EB",
borderRadius: "48px",
height: "48px",
width: "48px",
display: "flex",
justifyContent: "center",
alignItems: "center",
color: "white",
marginRight: "16px",
}}>
<img src={props.iconsrc} alt="#" style={{ height: "24px", width: "auto" }} />
</div>
<p
style={{
fontStyle: "normal",
fontWeight: 500,
fontSize: "18px",
lineHeight: "20px",
color: "#black",
}}>
<>{props.translationString}</>
</p>
</div>
);
};

View File

@ -11,6 +11,7 @@ export type TeamInvite = {
to: string;
teamName: string;
joinLink: string;
isCalcomMember: boolean;
};
export default class TeamInviteEmail extends BaseEmail {

View File

@ -153,7 +153,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
color="primary"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
data-testid="invite-new-member-button">
{t("invite_new_member")}
{t("invite")}
</Button>
</DialogFooter>
</Form>

View File

@ -279,8 +279,6 @@ export const viewerTeamsRouter = router({
},
});
let inviteeUserId: number | undefined = invitee?.id;
if (!invitee) {
// liberal email match
@ -291,7 +289,7 @@ export const viewerTeamsRouter = router({
});
// valid email given, create User and add to team
const user = await ctx.prisma.user.create({
await ctx.prisma.user.create({
data: {
email: input.usernameOrEmail,
invitedTo: input.teamId,
@ -303,7 +301,6 @@ export const viewerTeamsRouter = router({
},
},
});
inviteeUserId = user.id;
const token: string = randomBytes(32).toString("hex");
@ -314,7 +311,6 @@ export const viewerTeamsRouter = router({
expires: new Date(new Date().setHours(168)), // +1 week
},
});
if (ctx?.user?.name && team?.name) {
await sendTeamInviteEmail({
language: translation,
@ -322,6 +318,7 @@ export const viewerTeamsRouter = router({
to: input.usernameOrEmail,
teamName: team.name,
joinLink: `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/teams`,
isCalcomMember: false,
});
}
} else {
@ -357,6 +354,7 @@ export const viewerTeamsRouter = router({
to: sendTo,
teamName: team.name,
joinLink: WEBAPP_URL + "/settings/teams",
isCalcomMember: true,
});
}
}