| 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 | } |