diff --git a/canvas/src/app/orgs/page.tsx b/canvas/src/app/orgs/page.tsx index 653c196d..5f1787d6 100644 --- a/canvas/src/app/orgs/page.tsx +++ b/canvas/src/app/orgs/page.tsx @@ -21,6 +21,7 @@ import { useEffect, useState } from "react"; import { fetchSession, redirectToLogin, type Session } from "@/lib/auth"; import { PLATFORM_URL } from "@/lib/api"; import { formatCredits, pillTone, bannerKind } from "@/lib/credits"; +import { TermsGate } from "@/components/TermsGate"; type OrgStatus = "awaiting_payment" | "provisioning" | "running" | "failed" | string; @@ -162,17 +163,38 @@ function CheckoutBanner() { function Shell({ children }: { children: React.ReactNode }) { return (
-
-

Your organizations

-

- Each org is an isolated Molecule workspace. -

-
{children}
-
+ +
+

Your organizations

+

+ Each org is an isolated Molecule workspace. +

+ +
{children}
+
+
); } +// DataResidencyNotice surfaces where workspace data lives so EU-based +// signups can make an informed choice (GDPR Art. 13 disclosure +// requirement). Plain text, no icon — the goal is clarity, not +// decoration. A future EU region selector can replace this with a +// region dropdown. +function DataResidencyNotice() { + return ( +

+ Workspaces run in AWS us-east-2 (Ohio, United States). EU region support is on the roadmap — reach out to + {" "} + + support@moleculesai.app + + {" "}if you need data residency in another region today. +

+ ); +} + function OrgRow({ org }: { org: Org }) { return (
  • diff --git a/canvas/src/components/TermsGate.tsx b/canvas/src/components/TermsGate.tsx new file mode 100644 index 00000000..18a80518 --- /dev/null +++ b/canvas/src/components/TermsGate.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { PLATFORM_URL } from "@/lib/api"; + +// TermsGate blocks the page it wraps until the user has accepted the +// current terms version. Fetches /cp/auth/terms-status on mount; if +// the server says accepted=false it renders a modal over the children +// instead of hiding them entirely — that way the /orgs list is still +// visible behind the gate so the user understands what they're +// agreeing to touch. +// +// The server is the source of truth; this component is a UX +// convenience. Org-mutating endpoints should (and do) also enforce +// ToS via their own DB check so a power-user calling curl can't +// bypass the gate. +export function TermsGate({ children }: { children: React.ReactNode }) { + const [status, setStatus] = useState<"loading" | "accepted" | "pending" | "error">("loading"); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + (async () => { + try { + const res = await fetch(`${PLATFORM_URL}/cp/auth/terms-status`, { + credentials: "include", + signal: AbortSignal.timeout(10_000), + }); + if (cancelled) return; + if (res.status === 401) { + // Not signed in — the page this wraps handles redirect to login. + // Fall through to "accepted" so we don't double-gate anonymous. + setStatus("accepted"); + return; + } + if (!res.ok) { + setStatus("error"); + setError(`terms-status: ${res.status}`); + return; + } + const body = (await res.json()) as { accepted?: boolean }; + setStatus(body.accepted ? "accepted" : "pending"); + } catch (err) { + if (!cancelled) { + setStatus("error"); + setError(err instanceof Error ? err.message : String(err)); + } + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const accept = async () => { + setSubmitting(true); + setError(null); + try { + const res = await fetch(`${PLATFORM_URL}/cp/auth/accept-terms`, { + method: "POST", + credentials: "include", + signal: AbortSignal.timeout(10_000), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`${res.status}: ${text}`); + } + setStatus("accepted"); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + setSubmitting(false); + } + }; + + return ( + <> + {children} + {status === "pending" && ( +
    +
    +

    Terms & conditions

    +

    + Before you create an organization, please review our{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . Click agree to continue. +

    +

    + By agreeing you acknowledge that workspace data is stored in AWS us-east-2 (Ohio, United States). +

    + {error &&

    {error}

    } +
    + +
    +
    +
    + )} + {status === "error" && ( +
    + Couldn't check terms status: {error ?? "unknown error"} +
    + )} + + ); +}