Skip to content

Commit d3fdb4d

Browse files
Clarify team invitation emails and errors (#7325)
* Clarify team invitation emails * Clarify team invitation errors
1 parent ecb2b7b commit d3fdb4d

4 files changed

Lines changed: 84 additions & 18 deletions

File tree

apps/web/src/components/shared-note-viewer.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,18 @@ function SharedNoteMeetingMetadata({
188188
);
189189
}
190190

191-
export function SharedNoteLoading() {
191+
export function SharedNoteLoading({
192+
headerLabel = "Shared with Anarlog",
193+
loadingLabel = "Loading shared note",
194+
}: {
195+
headerLabel?: string;
196+
loadingLabel?: string;
197+
} = {}) {
192198
return (
193-
<SharedNoteShell>
199+
<SharedNoteShell headerLabel={headerLabel}>
194200
<div
195201
className="surface border-color-subtle rounded-3xl border px-6 py-8 sm:px-10"
196-
aria-label="Loading shared note"
202+
aria-label={loadingLabel}
197203
>
198204
<CircleNotch
199205
className="text-color-muted mb-6 size-5 animate-spin"
@@ -240,16 +246,18 @@ export function SharedNoteTransientError({ retry }: { retry?: () => void }) {
240246
export function SharedNotePrompt({
241247
actions,
242248
description,
249+
headerLabel = "Shared with Anarlog",
243250
icon,
244251
title,
245252
}: {
246253
actions?: React.ReactNode;
247254
description: string;
255+
headerLabel?: string;
248256
icon?: React.ReactNode;
249257
title: string;
250258
}) {
251259
return (
252-
<SharedNoteShell>
260+
<SharedNoteShell headerLabel={headerLabel}>
253261
<section className="surface border-color-subtle rounded-3xl border px-6 py-12 text-center sm:px-10">
254262
{icon && (
255263
<div className="text-color-muted mx-auto mb-4 flex justify-center">
@@ -272,9 +280,11 @@ export function SharedNotePrompt({
272280

273281
function SharedNoteShell({
274282
children,
283+
headerLabel = "Shared with Anarlog",
275284
topActions,
276285
}: {
277286
children: React.ReactNode;
287+
headerLabel?: string;
278288
topActions?: React.ReactNode;
279289
}) {
280290
const headerHidden = useSyncExternalStore(
@@ -296,7 +306,7 @@ function SharedNoteShell({
296306
<AnarlogLogo className="h-7 w-auto" />
297307
</a>
298308
{topActions ?? (
299-
<span className="text-xs text-stone-500">Shared with Anarlog</span>
309+
<span className="text-xs text-stone-500">{headerLabel}</span>
300310
)}
301311
</header>
302312
<div

apps/web/src/routes/team/invite/$invitationId.tsx

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {
2+
ArrowsClockwise,
23
CheckCircle,
34
Clock,
45
EnvelopeOpen,
56
Prohibit,
67
SignIn,
8+
WarningCircle,
79
} from "@phosphor-icons/react";
810
import { useMutation, useQuery } from "@tanstack/react-query";
911
import { ClientOnly, createFileRoute } from "@tanstack/react-router";
@@ -15,8 +17,6 @@ import {
1517
sharedSecondaryButtonClassName,
1618
SharedNoteLoading,
1719
SharedNotePrompt,
18-
SharedNoteTransientError,
19-
SharedNoteUnavailable,
2020
} from "@/components/shared-note-viewer";
2121
import { fetchUser } from "@/functions/auth";
2222
import { clearShareRouteContinuation } from "@/functions/share-route-continuation";
@@ -48,15 +48,15 @@ export const Route = createFileRoute("/team/invite/$invitationId")({
4848
],
4949
}),
5050
headers: () => privateShareHeaders,
51-
pendingComponent: SharedNoteLoading,
51+
pendingComponent: WorkspaceInvitationLoading,
5252
component: Component,
5353
});
5454

5555
function Component() {
5656
const { user } = Route.useLoaderData();
5757
const { invitationId } = Route.useParams();
5858
return (
59-
<ClientOnly fallback={<SharedNoteLoading />}>
59+
<ClientOnly fallback={<WorkspaceInvitationLoading />}>
6060
<InvitationClient invitationId={invitationId} signedIn={Boolean(user)} />
6161
</ClientOnly>
6262
);
@@ -116,12 +116,12 @@ function InvitationClient({
116116
});
117117

118118
if (continuation.isPending) {
119-
return <SharedNoteLoading />;
119+
return <WorkspaceInvitationLoading />;
120120
}
121121

122122
if (continuation.isError) {
123123
return (
124-
<SharedNoteTransientError
124+
<WorkspaceInvitationTransientError
125125
retry={() => {
126126
void continuation.retry();
127127
}}
@@ -130,7 +130,7 @@ function InvitationClient({
130130
}
131131

132132
if (!continuation.token || !parsedInvitationId) {
133-
return <SharedNoteUnavailable />;
133+
return <WorkspaceInvitationUnavailable />;
134134
}
135135

136136
if (!signedIn) {
@@ -140,6 +140,7 @@ function InvitationClient({
140140
});
141141
return (
142142
<SharedNotePrompt
143+
headerLabel="Team invitation"
143144
icon={<SignIn className="size-6" aria-hidden="true" />}
144145
title="Sign in to accept this invitation"
145146
description="Use the email address this workspace invitation was sent to. Your invitation stays in this browser tab while you sign in."
@@ -156,7 +157,17 @@ function InvitationClient({
156157
}
157158

158159
if (invitationQuery.isPending) {
159-
return <SharedNoteLoading />;
160+
return <WorkspaceInvitationLoading />;
161+
}
162+
163+
if (invitationQuery.isError || invitationQuery.data?.status === "error") {
164+
return (
165+
<WorkspaceInvitationTransientError
166+
retry={() => {
167+
void invitationQuery.refetch();
168+
}}
169+
/>
170+
);
160171
}
161172

162173
const invitationFailure = getInvitationRouteFailure({
@@ -168,14 +179,15 @@ function InvitationClient({
168179
invitationFailure === "unavailable" ||
169180
invitationQuery.data?.status !== "ready"
170181
) {
171-
return <SharedNoteUnavailable />;
182+
return <WorkspaceInvitationUnavailable />;
172183
}
173184

174185
const invitation = invitationQuery.data.invitation;
175186

176187
if (invitation.status === "accepted" || acceptMutation.isSuccess) {
177188
return (
178189
<SharedNotePrompt
190+
headerLabel="Team invitation"
179191
icon={<CheckCircle className="size-6" aria-hidden="true" />}
180192
title={`You've joined ${invitation.workspaceName}`}
181193
description="Open Anarlog on your computer to start collaborating in this workspace."
@@ -191,6 +203,7 @@ function InvitationClient({
191203
if (invitation.status === "revoked") {
192204
return (
193205
<SharedNotePrompt
206+
headerLabel="Team invitation"
194207
icon={<Prohibit className="size-6" aria-hidden="true" />}
195208
title="This invitation was revoked"
196209
description="Ask a workspace admin to send a new invitation."
@@ -201,6 +214,7 @@ function InvitationClient({
201214
if (invitation.status === "expired") {
202215
return (
203216
<SharedNotePrompt
217+
headerLabel="Team invitation"
204218
icon={<Clock className="size-6" aria-hidden="true" />}
205219
title="This invitation has expired"
206220
description="Ask a workspace admin to send a new invitation."
@@ -210,6 +224,7 @@ function InvitationClient({
210224

211225
return (
212226
<SharedNotePrompt
227+
headerLabel="Team invitation"
213228
icon={<EnvelopeOpen className="size-6" aria-hidden="true" />}
214229
title={`Join ${invitation.workspaceName}`}
215230
description="Accept the invitation to become a member of this Anarlog workspace."
@@ -245,6 +260,47 @@ function InvitationClient({
245260
);
246261
}
247262

263+
function WorkspaceInvitationLoading() {
264+
return (
265+
<SharedNoteLoading
266+
headerLabel="Team invitation"
267+
loadingLabel="Loading team invitation"
268+
/>
269+
);
270+
}
271+
272+
function WorkspaceInvitationUnavailable() {
273+
return (
274+
<SharedNotePrompt
275+
headerLabel="Team invitation"
276+
icon={<WarningCircle className="size-6" aria-hidden="true" />}
277+
title="This team invitation isn’t available"
278+
description="Sign in with the email address that received this invitation, or ask the workspace admin for a new invitation."
279+
/>
280+
);
281+
}
282+
283+
function WorkspaceInvitationTransientError({ retry }: { retry: () => void }) {
284+
return (
285+
<SharedNotePrompt
286+
headerLabel="Team invitation"
287+
icon={<WarningCircle className="size-6" aria-hidden="true" />}
288+
title="We couldn’t load this team invitation"
289+
description="Anarlog had a temporary problem loading the invitation. Please try again."
290+
actions={
291+
<button
292+
type="button"
293+
className={sharedPrimaryButtonClassName}
294+
onClick={retry}
295+
>
296+
<ArrowsClockwise className="mr-2 size-4" aria-hidden="true" />
297+
Try again
298+
</button>
299+
}
300+
/>
301+
);
302+
}
303+
248304
async function clearInvitationContinuation(pathname: string) {
249305
clearShareRouteToken(pathname);
250306
await clearShareRouteContinuation({ data: pathname }).catch(() => undefined);

crates/api-sync/src/shared_notes/email_delivery.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,9 @@ impl EmailDelivery {
188188
recipient,
189189
owner_email,
190190
&sender_name,
191-
format!("{sender_name} invited you to {workspace_name}"),
191+
format!("You're invited to join the \"{workspace_name}\" team in Anarlog"),
192192
format!(
193-
"{} invited you to join \"{}\" in Anarlog.\n\nAccept the invitation:\n{}\n\nReply to this email to contact {}.",
193+
"{} invited you to join \"{}\", a team in Anarlog.\n\nAccept the invitation:\n{}\n\nReply to this email to contact {}.",
194194
sender_name,
195195
workspace_name,
196196
invitation_url,

crates/api-sync/src/shared_notes/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ async fn verifies_and_sends_a_workspace_invitation_email() {
177177
"from": "Owner via Anarlog <notes@send.anarlog.so>",
178178
"to": "invitee@example.com",
179179
"reply_to": "owner@example.com",
180-
"subject": "Owner invited you to Fastrepl",
181-
"text": "Owner invited you to join \"Fastrepl\" in Anarlog.\n\nAccept the invitation:\nhttps://anarlog.so/team/invite/66666666-6666-4666-8666-666666666666/#token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\nReply to this email to contact Owner."
180+
"subject": "You're invited to join the \"Fastrepl\" team in Anarlog",
181+
"text": "Owner invited you to join \"Fastrepl\", a team in Anarlog.\n\nAccept the invitation:\nhttps://anarlog.so/team/invite/66666666-6666-4666-8666-666666666666/#token=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\nReply to this email to contact Owner."
182182
})))
183183
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "id": "email-id" })))
184184
.expect(1)

0 commit comments

Comments
 (0)