{t( @@ -44,21 +59,14 @@ export function ConnectCallbackView({ if (state === "linked") { return (
{t( - "portal.accountLink.connect.callback.linked.body", - "This server is connected to your Stirling account.", + "portal.accountLink.connect.done.lede", + "This server now runs against your Stirling account.", )} -
{t( @@ -67,22 +75,19 @@ export function ConnectCallbackView({ )}
)} - +{t( @@ -91,11 +96,6 @@ export function ConnectCallbackView({ )}
) : null} -VITE_SUPABASE_URL{" "}
- {t("portal.accountLink.modal.loginNotConfigured.and", "and")}{" "}
- VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY{" "}
- {t(
- "portal.accountLink.modal.loginNotConfigured.after",
- "so this server can finish the connection when you come back.",
- )}
- + {t( + "portal.accountLink.connect.handoff.reauthLede", + "Your Stirling session expired. Signing in again keeps usage and billing visible. This server stays connected either way.", + )} +
+ ) : ( +VITE_SUPABASE_URL{" "}
+ {t("portal.accountLink.modal.loginNotConfigured.and", "and")}{" "}
+ VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY{" "}
+ {t(
+ "portal.accountLink.modal.loginNotConfigured.after",
+ "so this server can finish the connection when you come back.",
+ )}
+ TODO(#7712): the credits row promises a monthly allowance the billing model does not grant — + * {@code freeGrantUnits} is a one-time lifetime pool — so either the grant becomes recurring or the + * copy drops "per month" before this reaches customers. + */ +export function ConnectBenefitsSlide() { + const { t } = useTranslation(); + + const unlocks: { key: string; label: string; detail: string }[] = [ + { + key: "processor", + label: t( + "portal.accountLink.connect.benefits.processorLabel", + "Processor", + ), + detail: t( + "portal.accountLink.connect.benefits.processorDetail", + "Pipelines, policies, sources and audit", + ), + }, + { + key: "teams", + label: t("portal.accountLink.connect.benefits.teamsLabel", "Teams"), + detail: t( + "portal.accountLink.connect.benefits.teamsDetail", + "Free for up to 5 users", + ), + }, + { + key: "credits", + label: t("portal.accountLink.connect.benefits.creditsLabel", "Credits"), + detail: t( + "portal.accountLink.connect.benefits.creditsDetail", + "500 free per month", + ), + }, + ]; + + return ( +
+ {t( + "portal.accountLink.connect.handoff.going", + "Taking you to stirling.com", + )} +
+ +Self-hosted (this base): billing only makes sense once the instance has - * linked its SaaS account, so gate on link state — unlinked shows the link prompt; - * linked renders the (flavor-agnostic) Usage page and maps its callbacks onto the - * link/tier dimension: the wallet's subscription status refines the plan/tier - * badge, and a lapsed SaaS session re-opens the account-link re-auth. This keeps - * the "link" concept entirely out of the Usage page. The SaaS build shadows this - * with a passthrough — there is no linking there. + *
Usage must not render while unlinked: {@link onWalletLoaded} reports linked as a fact, and the
+ * browser can hold a SaaS session with no link to this server, so rendering it flipped the portal
+ * to linked.
*/
export function PortalBillingGate() {
- const { isLinked } = useLink();
const applyLinkFacts = useApplyLinkFacts();
const { openLinkModal } = useUI();
@@ -27,6 +24,9 @@ export function PortalBillingGate() {
);
const onReauth = useCallback(() => openLinkModal("reauth"), [openLinkModal]);
- if (!isLinked) return