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 | */ | |
| 8 | import type { Invite, InvitesOverview, User } from "@g1t/contracts"; | |
| 9 | ||
| 10 | import { type BringInto, bringIntoChoices, inviteDraft } from "./invites"; | |
| 11 | import { billing, identity } from "./services.server"; | |
| 12 | ||
| 13 | /** What the page's forms answer. */ | |
| 14 | export 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. */ | |
| 21 | export 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 | */ | |
| 31 | export 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. */ | |
| 40 | export 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.