@cosmoops/connect-sdk 0.7.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +662 -0
- package/README.md +376 -124
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +59 -5
- package/dist/cjs/client/components/ai-format.js +135 -0
- package/dist/cjs/client/components/ai-widgets.js +232 -0
- package/dist/cjs/client/components/app-shell.js +80 -45
- package/dist/cjs/client/components/apps-grid.js +3 -2
- package/dist/cjs/client/components/ask-ai.js +353 -0
- package/dist/cjs/client/components/confirm-dialog.js +40 -0
- package/dist/cjs/client/components/cursor-pagination.js +22 -0
- package/dist/cjs/client/components/filter-tabs.js +24 -0
- package/dist/cjs/client/components/grievance-notice.js +30 -0
- package/dist/cjs/client/components/link-button.js +17 -10
- package/dist/cjs/client/components/logo.js +4 -3
- package/dist/cjs/client/components/nav.js +42 -1
- package/dist/cjs/client/components/plan-pricing.js +27 -0
- package/dist/cjs/client/components/plans.js +1 -1
- package/dist/cjs/client/components/platform-gate.js +48 -0
- package/dist/cjs/client/components/shell-context.js +149 -0
- package/dist/cjs/client/components/side-nav.js +50 -24
- package/dist/cjs/client/components/sidebar.js +127 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/user-menu.js +122 -40
- package/dist/cjs/client/components/workspace-switcher.js +100 -0
- package/dist/cjs/client/components/workspace-type-gate.js +48 -0
- package/dist/cjs/client/index.js +98 -3
- package/dist/cjs/client/provider.js +100 -2
- package/dist/cjs/client/use-checkout.js +9 -5
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +25 -2
- package/dist/cjs/server/handler.js +237 -16
- package/dist/cjs/server/http.js +52 -26
- package/dist/cjs/server/resources/acting-as.js +16 -0
- package/dist/cjs/server/resources/ai.js +15 -2
- package/dist/cjs/server/resources/data.js +374 -0
- package/dist/cjs/server/resources/entitlements.js +41 -0
- package/dist/cjs/server/resources/identity.js +9 -3
- package/dist/cjs/server/resources/kv.js +84 -0
- package/dist/cjs/server/resources/platform.js +43 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/resources/workspaces.js +74 -0
- package/dist/cjs/store.js +152 -0
- package/dist/cjs/ui/empty-state.js +28 -0
- package/dist/cjs/ui/index.js +44 -0
- package/dist/cjs/ui/page-header.js +27 -0
- package/dist/cjs/ui/page-shell.js +44 -0
- package/dist/cjs/ui/section.js +20 -0
- package/dist/cjs/ui/setting-row.js +14 -0
- package/dist/cjs/ui/status-chip.js +17 -0
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +60 -6
- package/dist/esm/client/components/ai-format.js +129 -0
- package/dist/esm/client/components/ai-widgets.js +223 -0
- package/dist/esm/client/components/app-shell.js +85 -50
- package/dist/esm/client/components/apps-grid.js +3 -2
- package/dist/esm/client/components/ask-ai.js +350 -0
- package/dist/esm/client/components/confirm-dialog.js +37 -0
- package/dist/esm/client/components/cursor-pagination.js +19 -0
- package/dist/esm/client/components/filter-tabs.js +21 -0
- package/dist/esm/client/components/grievance-notice.js +27 -0
- package/dist/esm/client/components/link-button.js +17 -10
- package/dist/esm/client/components/logo.js +4 -3
- package/dist/esm/client/components/nav.js +41 -1
- package/dist/esm/client/components/plan-pricing.js +25 -0
- package/dist/esm/client/components/plans.js +2 -2
- package/dist/esm/client/components/platform-gate.js +45 -0
- package/dist/esm/client/components/shell-context.js +141 -0
- package/dist/esm/client/components/side-nav.js +49 -22
- package/dist/esm/client/components/sidebar.js +120 -0
- package/dist/esm/client/components/subscribe-dialog.js +3 -3
- package/dist/esm/client/components/subscription.js +2 -2
- package/dist/esm/client/components/user-menu.js +121 -41
- package/dist/esm/client/components/workspace-switcher.js +97 -0
- package/dist/esm/client/components/workspace-type-gate.js +45 -0
- package/dist/esm/client/index.js +58 -5
- package/dist/esm/client/provider.js +94 -2
- package/dist/esm/client/use-checkout.js +9 -5
- package/dist/esm/errors.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +25 -2
- package/dist/esm/server/handler.js +237 -16
- package/dist/esm/server/http.js +52 -26
- package/dist/esm/server/resources/acting-as.js +13 -0
- package/dist/esm/server/resources/ai.js +15 -2
- package/dist/esm/server/resources/data.js +369 -0
- package/dist/esm/server/resources/entitlements.js +41 -0
- package/dist/esm/server/resources/identity.js +9 -3
- package/dist/esm/server/resources/kv.js +80 -0
- package/dist/esm/server/resources/platform.js +39 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/resources/workspaces.js +70 -0
- package/dist/esm/store.js +141 -0
- package/dist/esm/ui/empty-state.js +25 -0
- package/dist/esm/ui/index.js +34 -0
- package/dist/esm/ui/page-header.js +24 -0
- package/dist/esm/ui/page-shell.js +41 -0
- package/dist/esm/ui/section.js +17 -0
- package/dist/esm/ui/setting-row.js +11 -0
- package/dist/esm/ui/status-chip.js +14 -0
- package/dist/theme.css +87 -0
- package/dist/types/client/components/access-gate.d.ts +17 -3
- package/dist/types/client/components/ai-format.d.ts +24 -0
- package/dist/types/client/components/ai-widgets.d.ts +42 -0
- package/dist/types/client/components/app-shell.d.ts +85 -31
- package/dist/types/client/components/apps-grid.d.ts +11 -2
- package/dist/types/client/components/ask-ai.d.ts +73 -0
- package/dist/types/client/components/confirm-dialog.d.ts +34 -0
- package/dist/types/client/components/cursor-pagination.d.ts +27 -0
- package/dist/types/client/components/filter-tabs.d.ts +26 -0
- package/dist/types/client/components/grievance-notice.d.ts +31 -0
- package/dist/types/client/components/link-button.d.ts +32 -9
- package/dist/types/client/components/logo.d.ts +18 -2
- package/dist/types/client/components/nav.d.ts +27 -3
- package/dist/types/client/components/plan-pricing.d.ts +10 -0
- package/dist/types/client/components/platform-gate.d.ts +53 -0
- package/dist/types/client/components/shell-context.d.ts +116 -0
- package/dist/types/client/components/side-nav.d.ts +28 -14
- package/dist/types/client/components/sidebar.d.ts +95 -0
- package/dist/types/client/components/user-menu.d.ts +70 -23
- package/dist/types/client/components/workspace-switcher.d.ts +49 -0
- package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
- package/dist/types/client/index.d.ts +36 -7
- package/dist/types/client/provider.d.ts +149 -3
- package/dist/types/client/use-checkout.d.ts +8 -4
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +25 -2
- package/dist/types/server/http.d.ts +14 -3
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/resources/acting-as.d.ts +11 -0
- package/dist/types/server/resources/ai.d.ts +9 -1
- package/dist/types/server/resources/data.d.ts +258 -0
- package/dist/types/server/resources/entitlements.d.ts +33 -1
- package/dist/types/server/resources/kv.d.ts +63 -0
- package/dist/types/server/resources/platform.d.ts +32 -0
- package/dist/types/server/resources/workspaces.d.ts +55 -0
- package/dist/types/store.d.ts +96 -0
- package/dist/types/types.d.ts +491 -13
- package/dist/types/ui/empty-state.d.ts +34 -0
- package/dist/types/ui/index.d.ts +40 -0
- package/dist/types/ui/page-header.d.ts +34 -0
- package/dist/types/ui/page-shell.d.ts +39 -0
- package/dist/types/ui/section.d.ts +26 -0
- package/dist/types/ui/setting-row.d.ts +18 -0
- package/dist/types/ui/status-chip.d.ts +36 -0
- package/package.json +13 -3
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Avatar, Button, Chip, Modal, Skeleton, ToggleButton, ToggleButtonGroup, } from "@heroui/react";
|
|
4
|
-
import { CreditCard, ExternalLink, Laptop, LogOut, Moon, ReceiptText, Sun, UserRound, } from "lucide-react";
|
|
4
|
+
import { ChevronsUpDown, CreditCard, ExternalLink, Laptop, LogOut, Moon, ReceiptText, Sun, UserRound, } from "lucide-react";
|
|
5
5
|
import { useConnect } from "../provider.js";
|
|
6
6
|
import { ConnectMark } from "../mark.js";
|
|
7
7
|
import { ConnectAccountSessions } from "./account-sessions.js";
|
|
8
|
+
import { m } from "./motion.js";
|
|
9
|
+
import { SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
8
10
|
import { ICON, MEDALLION, PANEL_FILL } from "./surfaces.js";
|
|
9
11
|
const THEME_OPTIONS = [
|
|
10
12
|
{ value: "light", label: "Light", icon: Sun },
|
|
@@ -19,6 +21,28 @@ function initials(name, email) {
|
|
|
19
21
|
function openExternal(url) {
|
|
20
22
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Connect's profile page, with the context it needs to offer a way back here.
|
|
26
|
+
*
|
|
27
|
+
* Connect answers `/profile` for any signed-in user, so the bare URL works on
|
|
28
|
+
* its own - these two params only add the "Back to <your app>" banner. It
|
|
29
|
+
* validates `return_to` against this `client_id`'s registered redirect
|
|
30
|
+
* origins and silently drops the banner if they don't match, so a wrong or
|
|
31
|
+
* stale `appUrl`/`clientId` pairing degrades to what it did before rather
|
|
32
|
+
* than erroring.
|
|
33
|
+
*
|
|
34
|
+
* `window.location.href` rather than a configured home URL: the point is to
|
|
35
|
+
* come back to the page they left, and only the browser knows which one that
|
|
36
|
+
* is.
|
|
37
|
+
*/
|
|
38
|
+
function profileUrl(appUrl, clientId) {
|
|
39
|
+
const url = new URL("/profile", appUrl);
|
|
40
|
+
url.searchParams.set("client_id", clientId);
|
|
41
|
+
if (typeof window !== "undefined") {
|
|
42
|
+
url.searchParams.set("return_to", window.location.href);
|
|
43
|
+
}
|
|
44
|
+
return url.toString();
|
|
45
|
+
}
|
|
22
46
|
/**
|
|
23
47
|
* The member's photo, with their initials underneath it.
|
|
24
48
|
*
|
|
@@ -31,7 +55,15 @@ function openExternal(url) {
|
|
|
31
55
|
* had set on their profile.
|
|
32
56
|
*/
|
|
33
57
|
function UserAvatar({ user, size, }) {
|
|
34
|
-
return (
|
|
58
|
+
return (
|
|
59
|
+
// `rounded-full` is an override, not decoration. HeroUI's `.avatar` is a
|
|
60
|
+
// squircle - `rounded-2xl` at `sm`, `rounded-3xl` at `lg` - which is right
|
|
61
|
+
// for the media component in general and wrong for a *person*: every other
|
|
62
|
+
// product a customer uses draws an account avatar as a circle, and a
|
|
63
|
+
// squircle sitting beside circular status dots reads as a rendering fault
|
|
64
|
+
// rather than a choice. The root is `overflow-hidden`, so rounding it here
|
|
65
|
+
// rounds the image and the fallback with it.
|
|
66
|
+
_jsxs(Avatar, { size: size, color: "accent", variant: "soft", className: "rounded-full", children: [user.photoUrl && _jsx(Avatar.Image, { alt: "", src: user.photoUrl }), _jsx(Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
|
|
35
67
|
}
|
|
36
68
|
/** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
|
|
37
69
|
function humanizeRole(role) {
|
|
@@ -86,55 +118,102 @@ function DestinationTile({ icon, label, url, }) {
|
|
|
86
118
|
return (_jsxs(Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className: `relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border ${PANEL_FILL} p-3 hover:border-accent hover:bg-surface-secondary`, onPress: () => openExternal(url), children: [_jsx(ExternalLink, { className: "absolute end-2 top-2 size-3.5 text-muted", "aria-hidden": "true" }), _jsx("span", { className: `flex ${MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), _jsx("span", { className: "text-xs font-medium text-foreground", children: label })] }));
|
|
87
119
|
}
|
|
88
120
|
/**
|
|
89
|
-
*
|
|
90
|
-
* destinations
|
|
91
|
-
*
|
|
121
|
+
* The account modal's contents - identity card, the three Connect
|
|
122
|
+
* destinations, an optional appearance control, this account's sessions, and
|
|
123
|
+
* sign-out.
|
|
124
|
+
*
|
|
125
|
+
* Extracted from the trigger it used to be welded to, so the same surface can
|
|
126
|
+
* be opened from the sidebar's account row (`ConnectUserButton`) *or* from a
|
|
127
|
+
* host's own control (`ConnectProfileModal`) without a second copy of it. It
|
|
128
|
+
* renders the `Modal.Backdrop` downwards only; whoever renders it supplies the
|
|
129
|
+
* `<Modal>` and decides how it opens.
|
|
92
130
|
*
|
|
93
131
|
* Profile/Billing/Connect open in a **new tab**, each marked with an
|
|
94
|
-
* external-link icon. They are a different application at a different
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
132
|
+
* external-link icon. They are a different application at a different origin,
|
|
133
|
+
* and navigating the host app away to reach them would lose whatever the user
|
|
134
|
+
* was doing here. Sign out is the only action that acts in place, which is why
|
|
135
|
+
* it sits alone in the footer rather than as a fourth item in the same list as
|
|
136
|
+
* three navigations.
|
|
137
|
+
*/
|
|
138
|
+
function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
|
|
139
|
+
const { session, signOut, appUrl, clientId } = useConnect();
|
|
140
|
+
const user = session?.user;
|
|
141
|
+
if (!user)
|
|
142
|
+
return null;
|
|
143
|
+
const label = user.displayName ?? user.email ?? "Account";
|
|
144
|
+
const role = session?.role ?? null;
|
|
145
|
+
const planChip = subscriptionChip(session?.subscription ?? null);
|
|
146
|
+
return (_jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { size: "md", scroll: "inside", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Account" }) }), _jsxs(Modal.Body, { className: "flex flex-col gap-5", children: [_jsxs("div", { className: `flex items-start gap-3 rounded-xl border border-border ${PANEL_FILL} p-4`, children: [_jsx(UserAvatar, { user: user, size: "lg" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && _jsx("p", { className: "truncate text-xs text-muted", children: user.email }), _jsx("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && (_jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && (_jsx(Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && (_jsx(Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(DestinationTile, { icon: _jsx(UserRound, { className: ICON, "aria-hidden": "true" }), label: "Profile", url: profileUrl(appUrl, clientId) }), _jsx(DestinationTile, { icon: _jsx(CreditCard, { className: ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), _jsx(DestinationTile, { icon: _jsx(ConnectMark, { className: ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), _jsx(ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
|
|
147
|
+
const [next] = [...keys];
|
|
148
|
+
if (typeof next === "string")
|
|
149
|
+
onThemeChange(next);
|
|
150
|
+
}, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => (_jsxs(ToggleButton, { id: value, children: [index > 0 && _jsx(ToggleButtonGroup.Separator, {}), _jsx(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), _jsx(ConnectAccountSessions, {})] }), _jsx(Modal.Footer, { children: _jsxs(Button, { slot: "close", fullWidth: true, variant: "tertiary", className: "text-danger", onPress: () => {
|
|
151
|
+
void signOut().then(onSignOut);
|
|
152
|
+
}, children: [_jsx(LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The account modal with **no trigger of its own**, driven by the host.
|
|
156
|
+
*
|
|
157
|
+
* For an application that already has an account control it wants to keep -
|
|
158
|
+
* an avatar in its own chrome, a "Manage account" row in its own settings
|
|
159
|
+
* page - and wants Connect's modal behind it rather than rebuilding identity,
|
|
160
|
+
* sessions and sign-out. `ConnectUserButton` is the same modal with the
|
|
161
|
+
* trigger supplied; reach for this one only when that trigger is in the way.
|
|
99
162
|
*
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
163
|
+
* Renders nothing when signed out, open or not - there is no account to show.
|
|
164
|
+
*/
|
|
165
|
+
export function ConnectProfileModal({ isOpen, onOpenChange, onSignOut, theme, onThemeChange, }) {
|
|
166
|
+
const { session } = useConnect();
|
|
167
|
+
if (!session?.user)
|
|
168
|
+
return null;
|
|
169
|
+
return (_jsx(Modal, { isOpen: isOpen, onOpenChange: onOpenChange, children: _jsx(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange }) }));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The signed-in account, as a control that opens the account modal.
|
|
103
173
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
174
|
+
* In `"row"` form this is the last thing in the sidebar, which is where an
|
|
175
|
+
* account control belongs: it is the one item in the rail that is about *you*
|
|
176
|
+
* rather than about the product, and the foot of a column is where every
|
|
177
|
+
* application a customer already uses puts that. It collapses with the rail -
|
|
178
|
+
* the avatar holds its position and the name fades - because the avatar alone
|
|
179
|
+
* is still a complete account control, which is exactly what an icon rail
|
|
180
|
+
* needs.
|
|
181
|
+
*
|
|
182
|
+
* Renders nothing when signed out. Pair it with `<ConnectLoginButton>`, which
|
|
183
|
+
* renders nothing when signed *in*, and the two swap with no branch in your
|
|
184
|
+
* own layout.
|
|
107
185
|
*
|
|
108
186
|
* On the HeroUI shape: the trigger is a plain `<Button>` as a **direct child**
|
|
109
|
-
* of `<Modal>`, not wrapped in `<Modal.Trigger>`. `Dropdown.Trigger`
|
|
110
|
-
* the same construction
|
|
111
|
-
* wrapping one produces a button inside a button
|
|
112
|
-
* handling stops working.
|
|
113
|
-
* matching `<ConnectAppsGrid>`'s own `<Modal>` - nothing here needs to close
|
|
114
|
-
* the modal programmatically; sign-out uses `slot="close"` instead.
|
|
187
|
+
* of `<Modal>`, not wrapped in `<Modal.Trigger>`. `Dropdown.Trigger` - and by
|
|
188
|
+
* the same construction `Modal.Trigger` - is itself a React Aria `Button`, so
|
|
189
|
+
* wrapping one produces a button inside a button: invalid markup, and press
|
|
190
|
+
* handling stops working.
|
|
115
191
|
*/
|
|
116
|
-
export function
|
|
117
|
-
const { session, isLoading
|
|
192
|
+
export function ConnectUserButton({ variant = "row", onSignOut, className, theme, onThemeChange, }) {
|
|
193
|
+
const { session, isLoading } = useConnect();
|
|
194
|
+
const { collapsed } = useConnectShell();
|
|
118
195
|
if (isLoading) {
|
|
119
|
-
return (_jsxs("div", { className: `flex items-center gap-
|
|
196
|
+
return (_jsxs("div", { className: `flex h-10 items-center gap-3 ${variant === "row" ? "ps-[4px]" : ""} ${className ?? ""}`, children: [_jsx(Skeleton, { className: "size-8 shrink-0 rounded-full" }), variant !== "compact" && !collapsed && _jsx(Skeleton, { className: "h-4 w-24" })] }));
|
|
120
197
|
}
|
|
121
198
|
const user = session?.user;
|
|
122
199
|
if (!user)
|
|
123
200
|
return null;
|
|
124
201
|
const label = user.displayName ?? user.email ?? "Account";
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
202
|
+
return (_jsxs(Modal, { children: [variant === "row" ? (_jsxs(Button, { variant: "tertiary", "aria-label": `Account: ${label}`, className: sidebarRowClass({ tile: true, className }), children: [_jsx(UserAvatar, { user: user, size: "sm" }), _jsx(SidebarLabel, { children: _jsxs("span", { className: "flex min-w-0 flex-col text-start leading-tight", children: [_jsx("span", { className: "truncate text-sm font-medium text-foreground", children: label }), user.email && user.email !== label && (_jsx("span", { className: "truncate text-xs font-normal text-muted", children: user.email }))] }) }), _jsx(m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
|
|
203
|
+
// `ps-1` against the default trailing padding is optical, not
|
|
204
|
+
// sloppy: a circular avatar needs less inset than a text label to
|
|
205
|
+
// look evenly seated in the button.
|
|
206
|
+
className: `gap-2 ps-1 ${className ?? ""}`, children: [_jsx(UserAvatar, { user: user, size: "sm" }), variant === "inline" && _jsx("span", { className: "text-sm font-medium text-foreground", children: label })] })), _jsx(ProfileSurface, { onSignOut: onSignOut, theme: theme, onThemeChange: onThemeChange })] }));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* @deprecated Renamed to `ConnectUserButton` in 0.12 - it is one button that
|
|
210
|
+
* opens a modal, and calling it a "menu" had people looking for a dropdown
|
|
211
|
+
* that never existed. `compact` maps to `variant="compact"`, and the old
|
|
212
|
+
* default (avatar plus name) to `variant="inline"`. This alias will be removed
|
|
213
|
+
* in a later minor.
|
|
214
|
+
*/
|
|
215
|
+
export function ConnectUserMenu({ compact = false, ...rest }) {
|
|
216
|
+
return _jsx(ConnectUserButton, { variant: compact ? "compact" : "inline", ...rest });
|
|
138
217
|
}
|
|
139
218
|
/**
|
|
140
219
|
* The "Manage subscription" action on its own, for layouts that want it
|
|
@@ -167,13 +246,14 @@ export function ConnectInvoicesButton({ className }) {
|
|
|
167
246
|
* `ConnectHomeButton`.
|
|
168
247
|
*
|
|
169
248
|
* It was reachable only from inside `ConnectUserMenu`, which meant an app that
|
|
170
|
-
* builds its own account menu had to hardcode `${appUrl}/profile` to offer it
|
|
249
|
+
* builds its own account menu had to hardcode `${appUrl}/profile` to offer it -
|
|
250
|
+
* and got no `return_to`, so the user had no way back.
|
|
171
251
|
*/
|
|
172
252
|
export function ConnectProfileButton({ className }) {
|
|
173
|
-
const { session, appUrl } = useConnect();
|
|
253
|
+
const { session, appUrl, clientId } = useConnect();
|
|
174
254
|
if (!session?.user)
|
|
175
255
|
return null;
|
|
176
|
-
return (_jsxs(Button, { variant: "tertiary", className: className, onPress: () => openExternal(
|
|
256
|
+
return (_jsxs(Button, { variant: "tertiary", className: className, onPress: () => openExternal(profileUrl(appUrl, clientId)), children: [_jsx(UserRound, { className: "size-4 shrink-0" }), "Profile", _jsx(ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
|
|
177
257
|
}
|
|
178
258
|
/** A plain "Open Connect" button, for a footer or a help menu. */
|
|
179
259
|
export function ConnectHomeButton({ className }) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button, Dropdown, ListBox } from "@heroui/react";
|
|
5
|
+
import { ChevronsUpDown } from "lucide-react";
|
|
6
|
+
import { useConnect, useConnectUser, useConnectWorkspaces } from "../provider.js";
|
|
7
|
+
import { m } from "./motion.js";
|
|
8
|
+
import { SidebarLabel, sidebarRowClass, useConnectShell } from "./shell-context.js";
|
|
9
|
+
/** `Acme Logistics` -> `AL`. The collapsed rail's whole label for a workspace. */
|
|
10
|
+
function workspaceInitials(name) {
|
|
11
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
12
|
+
return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Switches which workspace this application is looking at.
|
|
16
|
+
*
|
|
17
|
+
* **Rendered in the `AppShell` sidebar header by default**, directly
|
|
18
|
+
* under the brand - which workspace you are in scopes everything below it, so
|
|
19
|
+
* it reads as a heading for the nav rather than as one more destination.
|
|
20
|
+
*
|
|
21
|
+
* It decides for itself whether to appear: an account with one workspace is
|
|
22
|
+
* shown nothing, because a switcher offering one option teaches the user
|
|
23
|
+
* nothing and costs a row of chrome on every screen. That is the opposite of
|
|
24
|
+
* Connect's own switcher, which always renders - Connect is where a workspace
|
|
25
|
+
* is *created*, so hiding it at one workspace would guarantee nobody ever
|
|
26
|
+
* reached two. A partner application creates nothing, so it has no such duty.
|
|
27
|
+
*
|
|
28
|
+
* **Authorization is the data, not a prop.** The list comes from the caller's
|
|
29
|
+
* own session (`GET /workspaces` takes no user id), so a workspace the user
|
|
30
|
+
* may not reach is simply not in it. There is deliberately no `roles` prop to
|
|
31
|
+
* pass or forget - the only way to render a row is for Connect to have
|
|
32
|
+
* returned it.
|
|
33
|
+
*
|
|
34
|
+
* Rows the user *belongs to* but cannot use here - a workspace outside the
|
|
35
|
+
* application's audience, or one with no subscription - are shown **disabled
|
|
36
|
+
* with the reason** rather than hidden. Hiding them reads as data loss to the
|
|
37
|
+
* person who owns the workspace; `switch` refuses the same two cases, so the
|
|
38
|
+
* label and the outcome always agree.
|
|
39
|
+
*
|
|
40
|
+
* A switch rotates the session token. The bridge persists the new one in the
|
|
41
|
+
* host application's own session store and the token never reaches this
|
|
42
|
+
* component - so the only thing to do afterwards is reload, which is what
|
|
43
|
+
* `onSwitched` defaults to.
|
|
44
|
+
*/
|
|
45
|
+
export function ConnectWorkspaceSwitcher({ onSwitched, row = true, className, }) {
|
|
46
|
+
const { mutateResource } = useConnect();
|
|
47
|
+
const { collapsed } = useConnectShell();
|
|
48
|
+
const user = useConnectUser();
|
|
49
|
+
const { data, isLoading } = useConnectWorkspaces();
|
|
50
|
+
const [switching, setSwitching] = useState(null);
|
|
51
|
+
const [error, setError] = useState(null);
|
|
52
|
+
const workspaces = data?.workspaces ?? [];
|
|
53
|
+
// Nothing to switch between, or nothing loaded yet. Deliberately renders
|
|
54
|
+
// `null` rather than a skeleton: this sits in persistent chrome, and a
|
|
55
|
+
// placeholder that resolves to nothing for most accounts is worse than
|
|
56
|
+
// appearing a moment late for the few who have two.
|
|
57
|
+
if (isLoading || workspaces.length < 2)
|
|
58
|
+
return null;
|
|
59
|
+
const active = workspaces.find((workspace) => workspace.isActive);
|
|
60
|
+
const label = active?.name ?? user?.workspaceName ?? "Workspace";
|
|
61
|
+
const handleSwitch = async (workspace) => {
|
|
62
|
+
if (workspace.isActive || !selectable(workspace))
|
|
63
|
+
return;
|
|
64
|
+
setSwitching(workspace.workspaceId);
|
|
65
|
+
setError(null);
|
|
66
|
+
try {
|
|
67
|
+
// Through the provider's own bridge caller rather than a bare `fetch`,
|
|
68
|
+
// so this inherits the configured `basePath`, the credentials mode and
|
|
69
|
+
// the error shape every other call in the SDK uses.
|
|
70
|
+
await mutateResource("/workspaces/switch", { workspaceId: workspace.workspaceId });
|
|
71
|
+
if (onSwitched) {
|
|
72
|
+
onSwitched(workspace);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
window.location.reload();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
setError(cause instanceof Error ? cause.message : "Could not switch workspace");
|
|
80
|
+
setSwitching(null);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return (_jsxs("div", { className: row ? "" : className, children: [_jsxs(Dropdown, { children: [row ? (
|
|
84
|
+
// A 32px tile rather than a 20px glyph, so the initials land on the
|
|
85
|
+
// same optical centre the avatar below them uses - see
|
|
86
|
+
// `SIDEBAR_ROW_TILE`. Collapsed, this tile *is* the switcher.
|
|
87
|
+
_jsxs(Button, { variant: "tertiary", "aria-label": `Workspace: ${label}. Switch workspace`, className: sidebarRowClass({ tile: true, className }), children: [_jsx("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-xs font-semibold text-accent", children: workspaceInitials(label) }), _jsx(SidebarLabel, { className: "text-sm font-medium text-foreground", children: label }), _jsx(m.span, { className: "shrink-0 text-muted", animate: { opacity: collapsed ? 0 : 1 }, transition: collapsed ? { duration: 0.1 } : { duration: 0.18, delay: 0.1 }, "aria-hidden": true, children: _jsx(ChevronsUpDown, { className: "size-4" }) })] })) : (_jsxs(Button, { variant: "secondary", size: "sm", className: "w-full justify-between", children: [_jsx("span", { className: "truncate", children: label }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 text-muted", "aria-hidden": "true" })] })), _jsx(Dropdown.Popover, { children: _jsx(ListBox, { children: workspaces.map((workspace) => (_jsxs(ListBox.Item, { id: workspace.workspaceId, textValue: workspace.name, isDisabled: !selectable(workspace) || switching !== null, onAction: () => void handleSwitch(workspace), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "truncate text-sm", children: workspace.name }), selectable(workspace) ? null : (_jsx("p", { className: "truncate text-xs text-muted", children: reasonUnavailable(workspace) }))] }), _jsx(ListBox.ItemIndicator, {})] }, workspace.workspaceId))) }) })] }), error && !collapsed ? _jsx("p", { className: "mt-1 px-2 text-xs text-danger", children: error }) : null] }));
|
|
88
|
+
}
|
|
89
|
+
/** Belongs to the user, and usable here. The same two facts `switch` enforces. */
|
|
90
|
+
function selectable(workspace) {
|
|
91
|
+
return workspace.inAudience && workspace.subscribed;
|
|
92
|
+
}
|
|
93
|
+
function reasonUnavailable(workspace) {
|
|
94
|
+
if (!workspace.inAudience)
|
|
95
|
+
return "Not available for this kind of workspace";
|
|
96
|
+
return "No active subscription";
|
|
97
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useConnect } from "../provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* Renders `children` only for a workspace of the given type.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <ConnectWorkspaceTypeGate type="business">
|
|
9
|
+
* <TeamMembersTable />
|
|
10
|
+
* </ConnectWorkspaceTypeGate>
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* The point is dead controls, not access: an `"individual"` workspace has
|
|
14
|
+
* exactly one seat and can never have another, so a members table, an invite
|
|
15
|
+
* button and a seat meter are all things that can only ever fail there.
|
|
16
|
+
* Hiding them is a better answer than letting someone press one and read an
|
|
17
|
+
* error about an account shape they did not choose.
|
|
18
|
+
*
|
|
19
|
+
* This is emphatically **not** the audience gate. It does not know what your
|
|
20
|
+
* application is sold to and will happily render a business-only subtree to a
|
|
21
|
+
* business workspace that has no business being in your application at all -
|
|
22
|
+
* that is `<ConnectAccessGate>`'s job, off the `audience_mismatch` readiness
|
|
23
|
+
* blocker. Use this one *inside* an application both audiences can reach, to
|
|
24
|
+
* vary what each sees.
|
|
25
|
+
*
|
|
26
|
+
* Like every gate in this package it is a **UX gate, not a security
|
|
27
|
+
* boundary**: the browser can ignore it. Anything that must not happen for an
|
|
28
|
+
* individual workspace has to be refused on your server too.
|
|
29
|
+
*
|
|
30
|
+
* Renders `loading` - not `fallback` - while the session is unknown, so a
|
|
31
|
+
* business account never flickers through the individual-account layout.
|
|
32
|
+
*/
|
|
33
|
+
export function ConnectWorkspaceTypeGate({ type, children, fallback = null, loading = null, }) {
|
|
34
|
+
const { session, isLoading } = useConnect();
|
|
35
|
+
const workspaceType = session?.user?.workspaceType ?? null;
|
|
36
|
+
if (isLoading)
|
|
37
|
+
return _jsx(_Fragment, { children: loading });
|
|
38
|
+
// Signed out is not "the other type" - there is no workspace to be a type
|
|
39
|
+
// of yet, and rendering the fallback here would put an individual-account
|
|
40
|
+
// layout in front of a visitor who has not signed in.
|
|
41
|
+
if (!workspaceType)
|
|
42
|
+
return _jsx(_Fragment, { children: loading });
|
|
43
|
+
const accepted = Array.isArray(type) ? type : [type];
|
|
44
|
+
return _jsx(_Fragment, { children: accepted.includes(workspaceType) ? children : fallback });
|
|
45
|
+
}
|
package/dist/esm/client/index.js
CHANGED
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
* whatever theme the host application platform already configured, and one theme
|
|
12
12
|
* change lands identically in all of them.
|
|
13
13
|
*/
|
|
14
|
-
export { ConnectProvider, useConnect, useConnectUser, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
|
|
14
|
+
export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectPlatformAccess, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectCompanyProfile, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
|
|
15
15
|
// Re-exported from the root entry rather than kept there alone: it is a pure
|
|
16
16
|
// helper, but every caller is UI code, and making a component file reach two
|
|
17
17
|
// entry points for one function is friction with nothing behind it.
|
|
18
18
|
export { blockingBlockers, normalizePlanFeatures } from "../helpers.js";
|
|
19
19
|
export { ConnectError, isConnectError } from "../errors.js";
|
|
20
|
+
export { ConnectGrievanceNotice } from "./components/grievance-notice.js";
|
|
20
21
|
export { ConnectMark } from "./mark.js";
|
|
21
22
|
export { ConnectLogo } from "./components/logo.js";
|
|
22
23
|
export { ConnectLoginButton, ConnectRegisterButton } from "./components/auth-buttons.js";
|
|
23
|
-
export {
|
|
24
|
+
export { ConnectUserButton, ConnectProfileModal,
|
|
25
|
+
/** @deprecated Renamed to `ConnectUserButton`. */
|
|
26
|
+
ConnectUserMenu, ConnectManageSubscriptionButton, ConnectInvoicesButton, ConnectProfileButton, ConnectHomeButton, ConnectSignOutButton, } from "./components/user-menu.js";
|
|
24
27
|
// Built, compiled into `dist/`, and reachable from no entry point until now -
|
|
25
28
|
// `ConnectUserMenu` was the only thing that could render it.
|
|
26
29
|
export { ConnectAccountSessions } from "./components/account-sessions.js";
|
|
@@ -28,15 +31,42 @@ export { ConnectAccountSessions } from "./components/account-sessions.js";
|
|
|
28
31
|
// pieces it composes rather than in a section of its own - an integration
|
|
29
32
|
// reaching for a sidebar wants to find the launcher and the account menu in
|
|
30
33
|
// the same breath, since the shell already mounts both.
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
/*
|
|
35
|
+
* `AppShell`, deliberately without the `Connect` prefix every other export
|
|
36
|
+
* here carries. That exception was decided in 0.8.0 and is kept: a host whose
|
|
37
|
+
* own layout component is also called `AppShell` aliases it on import
|
|
38
|
+
* (`import { AppShell as ConnectAppShell }`), which is the documented answer
|
|
39
|
+
* and what both reference integrations do.
|
|
40
|
+
*/
|
|
41
|
+
export { AppShell } from "./components/app-shell.js";
|
|
42
|
+
/*
|
|
43
|
+
* The rail, in pieces. `AppShell` composes exactly these and nothing
|
|
44
|
+
* else, which is the point of exporting them: an application whose chrome the
|
|
45
|
+
* prop API cannot express rebuilds the same rail from the same parts rather
|
|
46
|
+
* than forking it, and gets the collapse animation, the 32px glyph centre and
|
|
47
|
+
* the mobile drawer for free. They coordinate through `ConnectShellProvider`,
|
|
48
|
+
* so a part dropped in anywhere under one still knows the rail's state.
|
|
49
|
+
*/
|
|
50
|
+
export { ConnectSidebar, ConnectSidebarHeader, ConnectSidebarBody, ConnectSidebarFooter, ConnectSidebarRow, } from "./components/sidebar.js";
|
|
51
|
+
export { ConnectShellProvider, useConnectShell, sidebarRowClass, RAIL_COLLAPSED, RAIL_EXPANDED, } from "./components/shell-context.js";
|
|
52
|
+
export { ConnectSidebarNav, ConnectSideNav } from "./components/side-nav.js";
|
|
33
53
|
export { useActivePath } from "./use-active-path.js";
|
|
34
54
|
// The nav model is plain data and pure functions - a host generating its nav
|
|
35
55
|
// from its own route table needs the types, and `resolveActivePath` is the
|
|
36
56
|
// same longest-prefix rule the sidebar highlights with, so a breadcrumb or a
|
|
37
57
|
// page title derived from it cannot disagree with the rail.
|
|
38
|
-
export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemActive, gateExemptPaths, } from "./components/nav.js";
|
|
58
|
+
export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemActive, gateExemptPaths, filterNavForAudience, } from "./components/nav.js";
|
|
39
59
|
export { ConnectAppsGrid } from "./components/apps-grid.js";
|
|
60
|
+
// `AppShell askAi` mounts this; exported for an app that wants the panel
|
|
61
|
+
// without the frame, or wants it somewhere other than the shell's flex row
|
|
62
|
+
// (at which point it overlays rather than pushes - see its doc comment).
|
|
63
|
+
export { ConnectAskAi } from "./components/ask-ai.js";
|
|
64
|
+
// The seven widgets an Ask AI answer can draw, and the formatter they share -
|
|
65
|
+
// exported so a partner rendering Connect's chat somewhere else entirely (a
|
|
66
|
+
// support console, an embedded assistant) gets the same table, chart and metric
|
|
67
|
+
// tiles rather than reimplementing them from the wire types.
|
|
68
|
+
export { AiTable, AiChart, AiMetrics, AiTimeline, AiDetails, AiSteps, AiSources, } from "./components/ai-widgets.js";
|
|
69
|
+
export { formatAiValue, isNumericAiFormat, aiStatusTone, AI_EMPTY_VALUE, AI_TONE_CHIP, AI_TONE_DOT, } from "./components/ai-format.js";
|
|
40
70
|
export { ConnectIntegrationsModal } from "./components/integrations-modal.js";
|
|
41
71
|
export { ConnectPlans } from "./components/plans.js";
|
|
42
72
|
export { ConnectSubscribeDialog } from "./components/subscribe-dialog.js";
|
|
@@ -49,8 +79,29 @@ export { ConnectSubscriptionGate, ConnectNoSubscriptionCard, ConnectSubscription
|
|
|
49
79
|
export { ConnectReadinessBanner, ConnectReadinessGate } from "./components/readiness.js";
|
|
50
80
|
export { ConnectAccessGate, useConnectAccessGate } from "./components/access-gate.js";
|
|
51
81
|
export { ConnectFeatureGate, useConnectFeature } from "./components/feature-gate.js";
|
|
82
|
+
export { ConnectWorkspaceTypeGate } from "./components/workspace-type-gate.js";
|
|
83
|
+
export { ConnectPlatformGate } from "./components/platform-gate.js";
|
|
52
84
|
export { ConnectUsageMeter } from "./components/usage-meter.js";
|
|
53
85
|
export { ConnectPlansPage } from "./components/plans-page.js";
|
|
86
|
+
/*
|
|
87
|
+
* The interactive half of the page-composition layer. Their presentational
|
|
88
|
+
* counterparts - `ConnectPageShell`, `ConnectSection`, `ConnectEmptyState`,
|
|
89
|
+
* `ConnectStatusChip`, `ConnectSettingRow` - live in
|
|
90
|
+
* `@cosmoops/connect-sdk/ui`, which is deliberately not a `"use client"`
|
|
91
|
+
* module so a page shell does not become a client boundary. These three hold
|
|
92
|
+
* state, so they belong here.
|
|
93
|
+
*/
|
|
94
|
+
/*
|
|
95
|
+
* Built and compiled into `dist/` since 0.8 and reachable from no entry point
|
|
96
|
+
* until now - the same oversight `ConnectAccountSessions` had. It is the only
|
|
97
|
+
* supported way to render a link that looks like a button: HeroUI v3's
|
|
98
|
+
* `Button` has no `as` prop, so `<Button as={Link}>` neither type checks nor
|
|
99
|
+
* works.
|
|
100
|
+
*/
|
|
101
|
+
export { ConnectLinkButton } from "./components/link-button.js";
|
|
102
|
+
export { ConnectConfirmDialog } from "./components/confirm-dialog.js";
|
|
103
|
+
export { ConnectCursorPagination } from "./components/cursor-pagination.js";
|
|
104
|
+
export { ConnectFilterTabs } from "./components/filter-tabs.js";
|
|
54
105
|
export { useConnectCheckout, buildCheckoutUrl } from "./use-checkout.js";
|
|
55
106
|
export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACTION_LABEL,
|
|
56
107
|
// The tier model, exported because a partner writing its own pricing grid
|
|
@@ -59,3 +110,5 @@ export { formatPrice, monthlyOf, resolveAction, annualSaving, discountLabel, ACT
|
|
|
59
110
|
// keeps stable across a rename - `planGroup` is the field that exists to
|
|
60
111
|
// answer this, and `groupPlansByTier` is how to read it.
|
|
61
112
|
groupPlansByTier, resolvePlanGroup, } from "./components/plan-pricing.js";
|
|
113
|
+
export { ConnectWorkspaceSwitcher } from "./components/workspace-switcher.js";
|
|
114
|
+
export { useConnectWorkspaces, useConnectWorkspaceMembers } from "./provider.js";
|
|
@@ -111,6 +111,20 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
111
111
|
const invalidateResource = useCallback((path) => {
|
|
112
112
|
cache.current.delete(`${basePath}${path}`);
|
|
113
113
|
}, [basePath]);
|
|
114
|
+
const streamResource = useCallback(async (path, body, options) => {
|
|
115
|
+
const response = await (fetchImpl ?? globalThis.fetch)(`${basePath}${path}`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
credentials: "same-origin",
|
|
118
|
+
headers: { accept: "text/event-stream", "content-type": "application/json" },
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
signal: options?.signal,
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const envelope = await response.json().catch(() => null);
|
|
124
|
+
throw new Error(envelope?.error?.message ?? `Connect bridge returned ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
return response;
|
|
127
|
+
}, [basePath, fetchImpl]);
|
|
114
128
|
const refresh = useCallback(async () => {
|
|
115
129
|
cache.current.clear();
|
|
116
130
|
try {
|
|
@@ -122,7 +136,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
122
136
|
// error, so a Login button still renders rather than the UI hanging on
|
|
123
137
|
// skeletons forever.
|
|
124
138
|
setResult({
|
|
125
|
-
session: { user: null, subscription: null,
|
|
139
|
+
session: { user: null, subscription: null, role: null, permissions: [], staff: false, audience: null },
|
|
126
140
|
error: cause instanceof Error ? cause.message : "Could not reach Connect",
|
|
127
141
|
});
|
|
128
142
|
}
|
|
@@ -171,7 +185,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
171
185
|
credentials: "same-origin",
|
|
172
186
|
}).catch(() => { });
|
|
173
187
|
cache.current.clear();
|
|
174
|
-
setResult({ session: { user: null, subscription: null,
|
|
188
|
+
setResult({ session: { user: null, subscription: null, role: null, permissions: [], staff: false, audience: null }, error: null });
|
|
175
189
|
}, [basePath, fetchImpl]);
|
|
176
190
|
const value = useMemo(() => ({
|
|
177
191
|
clientId,
|
|
@@ -185,6 +199,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
185
199
|
fetchResource,
|
|
186
200
|
mutateResource,
|
|
187
201
|
invalidateResource,
|
|
202
|
+
streamResource,
|
|
188
203
|
}), [
|
|
189
204
|
clientId,
|
|
190
205
|
appUrl,
|
|
@@ -197,6 +212,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
197
212
|
fetchResource,
|
|
198
213
|
mutateResource,
|
|
199
214
|
invalidateResource,
|
|
215
|
+
streamResource,
|
|
200
216
|
]);
|
|
201
217
|
return _jsx(ConnectContext.Provider, { value: value, children: children });
|
|
202
218
|
}
|
|
@@ -211,6 +227,51 @@ export function useConnect() {
|
|
|
211
227
|
export function useConnectUser() {
|
|
212
228
|
return useConnect().session?.user ?? null;
|
|
213
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Whether the signed-in workspace is a one-seat personal account or a team -
|
|
232
|
+
* `null` while the session is loading or signed out.
|
|
233
|
+
*
|
|
234
|
+
* Branch on this to stop rendering team UI at a solo account: a members
|
|
235
|
+
* table, an invite button and a seat meter are all dead controls on an
|
|
236
|
+
* `"individual"` workspace, which has exactly one seat and cannot gain
|
|
237
|
+
* another. `<ConnectWorkspaceTypeGate>` is the declarative form.
|
|
238
|
+
*/
|
|
239
|
+
export function useConnectWorkspaceType() {
|
|
240
|
+
return useConnect().session?.user?.workspaceType ?? null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Whether this account is the kind your application is sold to - `null` while
|
|
244
|
+
* the session is loading or signed out.
|
|
245
|
+
*
|
|
246
|
+
* `audience.matches === false` is a hard block, not a hint: readiness reports
|
|
247
|
+
* `audience_mismatch` and subscribing is refused. `<ConnectAccessGate>`
|
|
248
|
+
* already blocks the shell on it, so reach for this only when you need the
|
|
249
|
+
* verdict somewhere the gate does not run - a marketing page, say, or a
|
|
250
|
+
* sign-up CTA you would rather not show to an account that cannot complete it.
|
|
251
|
+
*/
|
|
252
|
+
export function useConnectAudience() {
|
|
253
|
+
return useConnect().session?.audience ?? null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Whether this session is one of Connect's own operators - see
|
|
257
|
+
* `SessionAccess.staff`.
|
|
258
|
+
*
|
|
259
|
+
* `isStaff` is the branch you want; `roles` narrows further when your operator
|
|
260
|
+
* console has tiers of its own (a `developer` seeing a read-only view of what
|
|
261
|
+
* a `platform_admin` can edit, say).
|
|
262
|
+
*
|
|
263
|
+
* **A rendering decision, not an access decision.** The browser can call this
|
|
264
|
+
* whatever it likes. Everything an operator console exposes must be re-checked
|
|
265
|
+
* server-side against `staff` from
|
|
266
|
+
* `client.identity.getUser(sessionToken)` - see `<ConnectPlatformGate>`, which
|
|
267
|
+
* says the same thing at more length.
|
|
268
|
+
*/
|
|
269
|
+
export function useConnectPlatformAccess() {
|
|
270
|
+
const { session, isLoading } = useConnect();
|
|
271
|
+
const staff = session?.staff === true;
|
|
272
|
+
const role = session?.role ?? null;
|
|
273
|
+
return { isStaff: staff, roles: staff && role ? [role] : [], isLoading };
|
|
274
|
+
}
|
|
214
275
|
/** Readiness gates and the live subscription, fetched on first use. */
|
|
215
276
|
export function useConnectEntitlements() {
|
|
216
277
|
return useConnectResource("/entitlements");
|
|
@@ -229,10 +290,41 @@ export function useConnectAddonUsage() {
|
|
|
229
290
|
export function useConnectPlans() {
|
|
230
291
|
return useConnectResource("/plans");
|
|
231
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The platform's legal identity and DPDP grievance channel.
|
|
295
|
+
*
|
|
296
|
+
* The one read here that does not depend on a session, so it resolves the
|
|
297
|
+
* same for a signed-out visitor - which is deliberate, because the moment a
|
|
298
|
+
* grievance channel most needs to be readable is on a public privacy page.
|
|
299
|
+
*
|
|
300
|
+
* Read it rather than copying it into your own configuration; see
|
|
301
|
+
* `CompanyProfile`. `<ConnectGrievanceNotice>` renders it for you.
|
|
302
|
+
*/
|
|
303
|
+
export function useConnectCompanyProfile() {
|
|
304
|
+
return useConnectResource("/company-profile");
|
|
305
|
+
}
|
|
232
306
|
/** The apps this workspace can open. */
|
|
233
307
|
export function useConnectApps() {
|
|
234
308
|
return useConnectResource("/apps");
|
|
235
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* The workspaces this user belongs to - what `<ConnectWorkspaceSwitcher>`
|
|
312
|
+
* renders.
|
|
313
|
+
*
|
|
314
|
+
* Empty or single-entry results are normal, not errors: most accounts have one
|
|
315
|
+
* workspace, and the switcher decides for itself whether that is worth
|
|
316
|
+
* drawing.
|
|
317
|
+
*/
|
|
318
|
+
export function useConnectWorkspaces() {
|
|
319
|
+
return useConnectResource("/workspaces");
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* The team of the workspace this session is on, with each member's permissions
|
|
323
|
+
* already resolved for this application.
|
|
324
|
+
*/
|
|
325
|
+
export function useConnectWorkspaceMembers() {
|
|
326
|
+
return useConnectResource("/workspaces/members");
|
|
327
|
+
}
|
|
236
328
|
/** Enabled Composio-hosted toolkits and this workspace's connection status for each. */
|
|
237
329
|
export function useConnectIntegrations() {
|
|
238
330
|
return useConnectResource("/integrations");
|