Skip to content
60 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'worktree-agent-a6da31375506fc440'1/**
2 * Invite a friend (Settings → Invites to g1t): loading a person's invites to
3 * g1t and the workspaces one can also invite its person to, and answering
4 * the page's forms. Identity checks everything again: people only, a
5 * confirmed address, the allowance, and for a workspace, the owner and the
6 * paid plan.
7 */
8import type { Invite, InvitesOverview, User } from "@g1t/contracts";
9
10import { type BringInto, bringIntoChoices, inviteDraft } from "./invites";
11import { billing, identity } from "./services.server";
12
13/** What the page's forms answer. */
14export type InviteActionData =
15 | { done: "created"; invite: Invite }
16 | { done: "revoked" | "resent"; id: string }
17 | { error: string; intent: string; id?: string }
18 | null;
19
20/** A person's invites to g1t, or null when they cannot make any yet (address unconfirmed) or identity did not answer. */
21export async function loadInvites(user: User): Promise<InvitesOverview | null> {
22 if (!user.verified) return null;
23 return identity.listInvites(user).catch(() => null);
24}
25
26/**
27 * The workspaces an invite to g1t can also invite its person to: the ones
28 * the person owns on a paid plan. A workspace whose plan cannot be checked
29 * is not offered, since identity would refuse it.
30 */
31export async function loadBringInto(user: User): Promise<BringInto[]> {
32 const memberships = user.workspaces ?? [];
33 const owned = memberships.filter((m) => m.role === "owner").map((m) => m.slug);
34 if (owned.length === 0) return [];
35 const free = await billing.freeWorkspaces(owned).catch(() => owned);
36 return bringIntoChoices(memberships, free);
37}
38
39/** Handles the page's intents: create, revoke and resend. Null for any other intent. */
40export async function inviteAction(user: User, form: FormData): Promise<InviteActionData> {
41 const intent = String(form.get("intent") ?? "");
42 const id = String(form.get("id") ?? "").trim();
43 switch (intent) {
44 case "create-invite": {
45 // A workspace goes with it only when its box is ticked and it is one offered.
46 const draft = inviteDraft(form, await loadBringInto(user));
47 if (!draft.email) return { error: "Enter your friend's email address.", intent };
48 const result = await identity.createInvite(user, draft);
49 return result.ok ? { done: "created", invite: result.value } : { error: result.error.message, intent };
50 }
51 case "revoke-invite":
52 case "resend-invite": {
53 if (!id) return { error: "Which invite?", intent };
54 const result = intent === "revoke-invite" ? await identity.revokeInvite(user, id) : await identity.resendInvite(user, id);
55 if (!result.ok) return { error: result.error.message, intent, id };
56 return { done: intent === "revoke-invite" ? "revoked" : "resent", id };
57 }
58 }
59 return null;
60}

This file's history is long; its oldest lines are credited to the oldest commit read.