Client
React components and hooks. Holds no credential — calls your own origin.
Generated from @cosmoops/connect-sdk/client's TypeScript source and its own doc comments - see Reference for the other entry points. Regenerated on every deploy; don't hand-edit.
Interfaces
Interface: AppShellProps
Properties
| Property | Type | Description |
|---|---|---|
name | string | This application's name, in the cosmo·ops VOICE lockup. See ConnectLogo. |
icon? | ReactNode | This application's glyph, drawn on the CosmoOps badge. Falls back to the Connect rings. |
homeHref? | string | Where the lockup links. Defaults to /. |
nav | ConnectNav | The sidebar's destinations - items, sections, or a mix. See ConnectNav. |
secondaryNav? | ConnectNav | A second nav group, pinned to the foot of the rail above the account row. For the routes a product has but does not want competing with its own: settings, help, changelog, status. They are the same ConnectNav shape and get the same active resolution and audience filtering - the only difference is where they sit, which is the difference between "what this product is for" and "everything else". A route belongs up top if a user goes there to do the job, and down here if they go there to change how the job works. Putting Settings in the primary list is what makes a nine-item rail out of a five-item product. |
activePath? | string | Current path. Pass usePathname() in Next; see ConnectSidebarNavProps.activePath. |
renderLink? | ConnectLinkRenderer | See ConnectLinkRenderer - how this app renders a link. Defaults to a plain <a>. |
sidebarFooter? | ReactNode | Extra rows pinned to the bottom of the sidebar, above the account row. See ConnectSidebarRow. |
workspaceSwitcher? | | false | { alwaysShow?: boolean; } | The workspace switcher under the brand. On by default, and it hides itself for an account with only one workspace - so the reason to pass false is not "most users have one" (already handled) but "this application renders its own". Pass { alwaysShow: true } instead of true to keep it mounted below two workspaces too - see ConnectWorkspaceSwitcherProps.alwaysShow for what that changes about the dropdown. |
footer? | ReactNode | Rendered below children, inside the scrolling content pane. |
defaultCollapsed? | boolean | Start the rail collapsed. Defaults to true - see the component's own doc comment. Ignored when collapsed is supplied, and overridden by a stored preference from the user's last visit. |
collapsed? | boolean | Drive the rail yourself. Supply onCollapsedChange with it. |
onCollapsedChange? | (collapsed: boolean) => void | - |
collapsible? | boolean | Remove the collapse control and pin the rail expanded. |
showAppsGrid? | boolean | The app launcher, in the sidebar footer. On by default. |
showAuthButtons? | boolean | "Continue with Connect" / "Create account" in the sidebar footer when signed out. On by default. |
theme? | ThemeMode | Passed to ConnectUserButton - the SDK has no theme provider, so this is host-controlled. |
onThemeChange? | (theme: ThemeMode) => void | - |
gate? | boolean | Wrap children in ConnectAccessGate - suspended, no plan, onboarding pending, payment due, decided once for the whole app. On by default, because the shell is exactly the "wrap your app content once" place that gate asks for, and an app that mounts a shell and forgets the gate is an app that lets a suspended workspace keep working. The chrome is never gated: the sidebar, the lockup, the account row and sign-out stay reachable in every blocked state, so a blocked customer can always get to the page that unblocks them. Routes flagged gateExempt on their nav item render outside it - see ConnectNavItem.gateExempt and gateExemptRoutes. |
gateExemptRoutes? | string[] | Extra ungated paths, on top of the ones flagged gateExempt in nav. For a route that is not in the sidebar at all - a /checkout/callback, say. Prefix-matched, same as the nav's active resolution. |
askAi? | boolean | ConnectAskAiProps | Mount ConnectAskAi - a chat panel grounded in the signed-in workspace's Context Index, streamed through your own bridge. Pass an object to override its heading, copy or markdown plugins. On by default, like gate, and for the same reason: a family of application platforms should answer questions the same way, and a feature every partner has to remember to switch on is a feature most of them do not have. It stays inert until it can actually work - the trigger renders nothing for a signed-out visitor, and a workspace whose Context Index is off gets a message in the panel rather than a broken answer. Set false for an application that should not offer it at all. |
className? | string | - |
contentClassName? | string | Classes on the scrolling content pane - override the default max-width and padding here. |
children | ReactNode | - |
Interface: BookingsQuery
Properties
| Property | Type | Description |
|---|---|---|
from | Date | - |
to | Date | - |
hostMemberId? | string | Only appointments this member is hosting. Omit for the whole workspace. |
limit? | number | - |
Interface: CheckoutUrlOptions
Properties
| Property | Type | Description |
|---|---|---|
appUrl | string | Connect's origin, from useConnect().appUrl. |
applicationId | string | - |
planId | string | - |
subscriptionId? | string | null | The live subscription's id, when there is one. Present means "change the plan on this subscription"; absent means "subscribe for the first time" - and the two are separate screens in Connect, because only one of them can price proration against a cycle already paid for. Pass it only when you know which plan the subscription is on today, which is what makes the change classifiable. checkoutSubscriptionId derives it from the action you are already showing on the button; a bare subscription?.subscription?.id does not, and a live subscription whose plan has been deleted from the catalogue is the case where the two differ and the change-plan screen answers notFound(). Leaving it null is always safe: the subscribe route redirects to the change-plan screen itself when Connect finds a subscription to change. |
coupon? | string | null | Carried into Connect's checkout, where it is validated against the real cart. |
seats? | number | null | Seats to subscribe with on a per-seat plan - the total, not extras. Seeds Connect's checkout stepper, where the plan's included count is the floor. Lets "Subscribe for my 8-person team" price eight seats in one payment rather than the included count now and a prorated top-up after. Ignored on a plan change (seats are changed on the subscription) and on plans that do not sell seats one at a time. |
next? | string | null | Where Connect sends the customer once the subscription is live - an absolute URL on your own origin. Without it they do not come back. Connect's default destination is the new subscription's own billing page, which is the right answer for someone who started in Connect and the wrong one for someone who started on your pricing page: they pay, land on a billing screen for a product they were in the middle of adopting, and nothing on it points home. Connect validates the origin against the redirect URIs registered for your Connect client and silently ignores anything else, so a value it does not recognise costs the return trip and never redirects your customer somewhere you did not register. Register the origin you intend to use; the path and query are yours. |
Interface: ComparablePlan
The minimum a plan-like object has to expose to be grouped or compared -
lets the dialog pass a SubscribedPlan (which carries no add-ons or
features) wherever a comparison is wanted.
Properties
| Property | Type |
|---|---|
name | string |
price | number |
billingInterval | BillingInterval |
planGroup? | string | null |
Interface: ConnectAccessGateProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | - |
fallback? | ReactNode | Replaces the default card for every hard-block state (suspended/no_subscription/onboarding_pending/blocked) uniformly. For a different card per state, call useConnectAccessGate() directly instead. |
loading? | ReactNode | Shown while the first read is in flight, in place of children. Opt-in, and rarely what you want. The default is to render children during that window - see the component's doc comment on why a blanket placeholder over a whole application is worse than the flash it avoids. Pass this only for a gate wrapped around one small region whose blocked and unblocked states differ enough that showing the unblocked one first would mislead. |
className? | string | - |
Interface: ConnectAccessGateResult
Properties
| Property | Type | Description |
|---|---|---|
state | ConnectAccessGateState | - |
blocker | ReadinessBlocker | null | The blocker driving a hard-block state (suspended/no_subscription/onboarding_pending/blocked). null otherwise. |
paymentRetry | ReadinessBlocker | null | The payment_retry_pending blocker, only when state === "payment_retry". Carries graceEndsAt. |
Methods
refresh()
refresh(): void;Re-reads entitlements, evicting the cached answer first.
Exposed because a blocker is almost always resolved somewhere this tab cannot see - on Connect, in another tab or on a phone - and nothing about that reaches this one. The gate wires it to the tab regaining focus; call it yourself if your app learns of a change some other way.
Returns
void
Interface: ConnectAccountSessionsProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | - |
maxVisible? | number | Rows shown before the "Show all" toggle collapses the rest. An account with a dozen devices would otherwise push everything below it - the sign-out action included - off the bottom of a modal. Pass 0 to always render every row. |
Interface: ConnectAppsGridProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | Extra classes on the trigger button. |
label? | string | Trigger label for screen readers, and the collapsed rail's tooltip. |
variant? | "icon" | "row" | How the trigger is drawn. - "row" - a full-width sidebar row, glyph on the rail's shared 32px centre. The default, since the launcher lives in the sidebar footer now that there is no top bar to hold it. - "icon" - the square accent button, for a bar of your own. |
Interface: ConnectAskAiProps
Properties
| Property | Type | Description |
|---|---|---|
screens? | readonly ConnectScreen[] | The screens in your application, described for the assistant. Without these it answers from general knowledge about software, because Connect has no idea what your product is - see ConnectScreen. With them, "what can I do here?" answers in your own words, the empty panel offers your own opening questions, and showActions can link into your routes. Prose and links only: Connect never uses them to decide what anyone may do, and never executes anything on your behalf. |
activePath? | string | The route the user is currently on. Usually omit this. Left unset, the panel resolves the live path itself through useActivePath, which reads location.pathname from a useSyncExternalStore whose server snapshot is empty - so it works inside a shared App Shell, where usePathname() is refused outright (CLIENT_HOOK_DYNAMIC) because URL data cannot live in an artifact shared across URLs. That is the same reason ConnectAppShell recommends leaving its own activePath unset in a Next app. Pass it only where your router's path is not location.pathname - a hash router, or a panel embedded under a basePath your routes do not include. |
renderLink? | (props: { href: string; className: string; children: ReactNode; }) => ReactNode | Renders an action link with your router, so following one is a client transition rather than a document load. Omit it and links are plain anchors, which work everywhere. renderLink={({ href, className, children }) => ( <Link href={href} className={className}>{children}</Link> )} |
title? | string | Panel heading. Defaults to "Ask AI". |
assistantName? | string | Byline over each assistant turn. Defaults to "Assistant". |
placeholder? | string | Composer placeholder. |
hint? | string | Shown once, above an empty conversation. |
defaultOpen? | boolean | Start open. The panel owns its state after that. |
markdownPlugins? | PluginConfig | Streamdown plugins - syntax highlighting, KaTeX, Mermaid, CJK. Off unless you pass them, because each is a package of its own and several are large (Mermaid alone dwarfs this SDK). Prose, GFM tables, task lists and plain code fences all render without any of them. import { code } from "@streamdown/code"; import { mermaid } from "@streamdown/mermaid"; <ConnectAskAi markdownPlugins={{ code, mermaid }} /> Hoist the object to module scope: Streamdown compares this prop by reference, so an inline literal defeats its block memoisation. |
Interface: ConnectAuthButtonProps
Extends
Omit<ButtonProps,"onPress"|"children">
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
variant? | | "danger" | "primary" | "secondary" | "danger-soft" | "ghost" | "outline" | "tertiary" | - | ConnectAuthButtonProps.variant |
fullWidth? | boolean | - | ConnectAuthButtonProps.fullWidth |
isIconOnly? | boolean | - | ConnectAuthButtonProps.isIconOnly |
size? | "lg" | "md" | "sm" | - | ConnectAuthButtonProps.size |
children? | ReactNode | Defaults to "Continue with Connect" / "Sign up with Connect". | - |
__button_group_child? | boolean | - | Omit.__button_group_child |
aria-label? | string | Defines a string value that labels the current element. | Omit.aria-label |
aria-labelledby? | string | Identifies the element (or elements) that labels the current element. | Omit.aria-labelledby |
aria-describedby? | string | Identifies the element (or elements) that describes the object. | Omit.aria-describedby |
aria-details? | string | Identifies the element (or elements) that provide a detailed, extended description for the object. | Omit.aria-details |
id? | string | The element's unique identifier. See MDN. | Omit.id |
excludeFromTabOrder? | boolean | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | Omit.excludeFromTabOrder |
dir? | string | - | Omit.dir |
lang? | string | - | Omit.lang |
hidden? | boolean | - | Omit.hidden |
inert? | boolean | - | Omit.inert |
translate? | "yes" | "no" | - | Omit.translate |
onClickCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onClickCapture |
onAuxClick? | MouseEventHandler<HTMLButtonElement> | - | Omit.onAuxClick |
onAuxClickCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onAuxClickCapture |
onContextMenu? | MouseEventHandler<HTMLButtonElement> | - | Omit.onContextMenu |
onContextMenuCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onContextMenuCapture |
onDoubleClick? | MouseEventHandler<HTMLButtonElement> | - | Omit.onDoubleClick |
onDoubleClickCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onDoubleClickCapture |
onMouseDown? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseDown |
onMouseDownCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseDownCapture |
onMouseEnter? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseEnter |
onMouseLeave? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseLeave |
onMouseMove? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseMove |
onMouseMoveCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseMoveCapture |
onMouseOut? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseOut |
onMouseOutCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseOutCapture |
onMouseOver? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseOver |
onMouseOverCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseOverCapture |
onMouseUp? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseUp |
onMouseUpCapture? | MouseEventHandler<HTMLButtonElement> | - | Omit.onMouseUpCapture |
onTouchCancel? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchCancel |
onTouchCancelCapture? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchCancelCapture |
onTouchEnd? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchEnd |
onTouchEndCapture? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchEndCapture |
onTouchMove? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchMove |
onTouchMoveCapture? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchMoveCapture |
onTouchStart? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchStart |
onTouchStartCapture? | TouchEventHandler<HTMLButtonElement> | - | Omit.onTouchStartCapture |
onPointerDown? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerDown |
onPointerDownCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerDownCapture |
onPointerMove? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerMove |
onPointerMoveCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerMoveCapture |
onPointerUp? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerUp |
onPointerUpCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerUpCapture |
onPointerCancel? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerCancel |
onPointerCancelCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerCancelCapture |
onPointerEnter? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerEnter |
onPointerLeave? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerLeave |
onPointerOver? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerOver |
onPointerOverCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerOverCapture |
onPointerOut? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerOut |
onPointerOutCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onPointerOutCapture |
onGotPointerCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onGotPointerCapture |
onGotPointerCaptureCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onGotPointerCaptureCapture |
onLostPointerCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onLostPointerCapture |
onLostPointerCaptureCapture? | PointerEventHandler<HTMLButtonElement> | - | Omit.onLostPointerCaptureCapture |
onScroll? | UIEventHandler<HTMLButtonElement> | - | Omit.onScroll |
onScrollCapture? | UIEventHandler<HTMLButtonElement> | - | Omit.onScrollCapture |
onWheel? | WheelEventHandler<HTMLButtonElement> | - | Omit.onWheel |
onWheelCapture? | WheelEventHandler<HTMLButtonElement> | - | Omit.onWheelCapture |
onAnimationStart? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationStart |
onAnimationStartCapture? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationStartCapture |
onAnimationEnd? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationEnd |
onAnimationEndCapture? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationEndCapture |
onAnimationIteration? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationIteration |
onAnimationIterationCapture? | AnimationEventHandler<HTMLButtonElement> | - | Omit.onAnimationIterationCapture |
onTransitionCancel? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionCancel |
onTransitionCancelCapture? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionCancelCapture |
onTransitionEnd? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionEnd |
onTransitionEndCapture? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionEndCapture |
onTransitionRun? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionRun |
onTransitionRunCapture? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionRunCapture |
onTransitionStart? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionStart |
onTransitionStartCapture? | TransitionEventHandler<HTMLButtonElement> | - | Omit.onTransitionStartCapture |
onKeyDown? | (e: KeyboardEvent) => void | Handler that is called when a key is pressed. | Omit.onKeyDown |
onKeyUp? | (e: KeyboardEvent) => void | Handler that is called when a key is released. | Omit.onKeyUp |
onFocus? | (e: FocusEvent<Element>) => void | Handler that is called when the element receives focus. | Omit.onFocus |
onBlur? | (e: FocusEvent<Element>) => void | Handler that is called when the element loses focus. | Omit.onBlur |
onFocusChange? | (isFocused: boolean) => void | Handler that is called when the element's focus status changes. | Omit.onFocusChange |
onHoverStart? | (e: HoverEvent) => void | Handler that is called when a hover interaction starts. | Omit.onHoverStart |
onHoverEnd? | (e: HoverEvent) => void | Handler that is called when a hover interaction ends. | Omit.onHoverEnd |
onHoverChange? | (isHovering: boolean) => void | Handler that is called when the hover state changes. | Omit.onHoverChange |
onPressStart? | (e: PressEvent) => void | Handler that is called when a press interaction starts. | Omit.onPressStart |
onPressEnd? | (e: PressEvent) => void | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | Omit.onPressEnd |
onPressChange? | (isPressed: boolean) => void | Handler that is called when the press state changes. | Omit.onPressChange |
onPressUp? | (e: PressEvent) => void | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | Omit.onPressUp |
onClick? | (e: MouseEvent<FocusableElement>) => void | Not recommended – use onPress instead. onClick is an alias for onPress provided for compatibility with other libraries. onPress provides additional event details for non-mouse interactions. | Omit.onClick |
autoFocus? | boolean | Whether the element should receive focus on render. | Omit.autoFocus |
key? | Key | null | - | Omit.key |
ref? | Ref<HTMLButtonElement> | Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). See React Docs | Omit.ref |
isDisabled? | boolean | Whether the button is disabled. | Omit.isDisabled |
aria-disabled? | boolean | "true" | "false" | Indicates whether the element is disabled to users of assistive technology. | Omit.aria-disabled |
aria-expanded? | boolean | "true" | "false" | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | Omit.aria-expanded |
aria-haspopup? | | boolean | "true" | "false" | "dialog" | "grid" | "listbox" | "menu" | "tree" | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | Omit.aria-haspopup |
aria-controls? | string | Identifies the element (or elements) whose contents or presence are controlled by the current element. | Omit.aria-controls |
aria-pressed? | boolean | "true" | "false" | "mixed" | Indicates the current "pressed" state of toggle buttons. | Omit.aria-pressed |
aria-current? | boolean | "date" | "location" | "true" | "false" | "time" | "page" | "step" | Indicates whether this element represents the current item within a container or set of related elements. | Omit.aria-current |
type? | "reset" | "submit" | "button" | The behavior of the button when used in an HTML form. Default 'button' | Omit.type |
preventFocusOnPress? | boolean | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | Omit.preventFocusOnPress |
form? | string | The <form> element to associate the button with. The value of this attribute must be the id of a <form> in the same document. See MDN. | Omit.form |
formAction? | string | ((formData: FormData) => void | Promise<void>) | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | Omit.formAction |
formEncType? | string | Indicates how to encode the form data that is submitted. | Omit.formEncType |
formMethod? | string | Indicates the HTTP method used to submit the form. | Omit.formMethod |
formNoValidate? | boolean | Indicates that the form is not to be validated when it is submitted. | Omit.formNoValidate |
formTarget? | string | Overrides the target attribute of the button's form owner. | Omit.formTarget |
name? | string | Submitted as a pair with the button's value as part of the form data. | Omit.name |
value? | string | The value associated with the button's name when it's submitted with the form data. | Omit.value |
className? | ClassNameOrFunction<ButtonRenderProps> | The CSS className for the element. A function may be provided to compute the class based on component state. Default 'react-aria-Button' | Omit.className |
isPending? | boolean | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | Omit.isPending |
style? | StyleOrFunction<ButtonRenderProps> | The inline style for the element. A function may be provided to compute the style based on component state. | Omit.style |
slot? | string | null | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit null value indicates that the local props completely override all props received from a parent. | Omit.slot |
render? | DOMRenderFunction<"button", ButtonRenderProps> | Overrides the default DOM element with a custom render function. This allows rendering existing components with built-in styles and behaviors such as router links, animation libraries, and pre-styled components. Requirements: - You must render the expected element type (e.g. if <button> is expected, you cannot render an <a>). - Only a single root DOM element can be rendered (no fragments). - You must pass through props and ref to the underlying DOM element, merging with your own prop as appropriate. | Omit.render |
Interface: ConnectAvailabilityFormModalProps
Properties
| Property | Type | Description |
|---|---|---|
isOpen | boolean | - |
onOpenChange | (isOpen: boolean) => void | - |
scheduleId? | string | null | null (or omitted) creates a new schedule. A row id from useAvailabilitySchedules edits that one instead - name and hours prefill from it. |
onSaved? | (schedule: AvailabilitySchedule) => void | Called once the schedule is created or saved, before the modal closes. |
Interface: ConnectBookingCalendarProps
Properties
| Property | Type | Description |
|---|---|---|
timeZone? | string | The business's IANA timezone. Every instant on the grid is read against it, never the viewer's - a workspace's working day must not shift because a member is travelling. Defaults to the host's zone, which is right for the common case where staff and business share one. Pass it explicitly when they might not. |
hostMemberId? | string | Only appointments this member hosts. Omit for the whole workspace's diary. |
window? | GridWindow | Narrow the visible band - see DEFAULT_WINDOW. |
canBook? | boolean | Hide the "New appointment" action for a read-only view. |
className? | string | - |
Interface: ConnectCallButtonProps
The number to ring, and how to name the person on the in-call bar.
Extends
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
phone | string | E.164, e.g. +919876543210. | ConnectCallTarget.phone |
name? | string | Shown on the in-call bar. Defaults to the number. | ConnectCallTarget.name |
purpose? | ConnectCallPurpose | What the call is for - Connect places it from a number allowed to carry it. See ConnectCallPurpose; omit it to place the call as before. | ConnectCallTarget.purpose |
onEnded? | (result: { callId: string | null; }) => void | Called once the call is over, with Connect's id for it - or null when it never got as far as being approved. Read the outcome on your server with connect.calls.get(callId); see the module comment for why not here. | ConnectCallTarget.onEnded |
variant? | "default" | "compact" | compact is an icon-only button for a row beside a number. | - |
children? | ReactNode | Button label. Defaults to "Call". | - |
isDisabled? | boolean | - | - |
className? | string | Merged with cn, so an override replaces the default it conflicts with. | - |
Interface: ConnectCallContextValue
Properties
| Property | Type | Description |
|---|---|---|
isBusy | boolean | A call is in progress - every Call button disables on it. |
availability | | CallingAvailability | null | Whether this member can call and, if not, why - null while it loads. Read once by the provider and shared, so every button and status panel agrees, and refreshed after each call so the minutes shown keep up. |
Methods
startCall()
startCall(target: ConnectCallTarget): Promise<void>;Parameters
| Parameter | Type |
|---|---|
target | ConnectCallTarget |
Returns
Promise<void>
refreshAvailability()
refreshAvailability(): void;Re-read availability - after a top-up in another tab, say.
Returns
void
Interface: ConnectCallingStatusProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | Merged with cn. |
Interface: ConnectCallProviderProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | - |
onCallEnded? | (result: { callId: string | null; phone: string; }) => void | Called for every call that ends, whichever button started it - the place to wire "log this call" once for the whole app. A button's own onEnded runs as well. |
onError? | (message: string) => void | Called when a call could not be placed, with the reason to show. Defaults to the in-call bar saying so. |
className? | string | Extra classes for the in-call bar's outer element - merged with cn. |
Interface: ConnectCallTarget
The number to ring, and how to name the person on the in-call bar.
Extended by
Properties
| Property | Type | Description |
|---|---|---|
phone | string | E.164, e.g. +919876543210. |
name? | string | Shown on the in-call bar. Defaults to the number. |
purpose? | ConnectCallPurpose | What the call is for - Connect places it from a number allowed to carry it. See ConnectCallPurpose; omit it to place the call as before. |
onEnded? | (result: { callId: string | null; }) => void | Called once the call is over, with Connect's id for it - or null when it never got as far as being approved. Read the outcome on your server with connect.calls.get(callId); see the module comment for why not here. |
Interface: ConnectCheckoutState
Properties
| Property | Type | Description |
|---|---|---|
status | ConnectCheckoutStatus | - |
url | string | null | The Connect URL of the run that produced this state - render it as a fallback link when status is "blocked". |
Interface: ConnectConfirmDialogProps
Properties
| Property | Type | Description |
|---|---|---|
trigger | ReactNode | The control that opens it, and it must be a pressable - a HeroUI Button, or anything else built on React Aria's useButton. It is rendered as a direct child of the Modal root, which is React Aria's DialogTrigger: that puts a PressResponder around its children and the trigger registers with it. A plain <span> never registers, and React Aria warns that the responder has no pressable child. |
title | ReactNode | - |
description? | ReactNode | What will happen, in the user's words. Name the thing being acted on. |
confirmLabel? | string | - |
cancelLabel? | string | - |
tone? | "danger" | "primary" | danger for anything that destroys data. |
onConfirm | () => void | Promise<void> | - |
Interface: ConnectContextValue
Properties
| Property | Type | Description |
|---|---|---|
clientId | string | Public OAuth client id - the clientId passed to <ConnectProvider>, else the one your bridge reports on /session, else an empty string (an older bridge, before the session read settles). Safe in a browser regardless: it only ever goes into Connect URLs. Most integrations never read this - it exists for the explicit-redirectUri flow and for connectPortalUrl's way back. |
appUrl | string | Connect's own origin, for deep links out to Profile, Billing and the catalog. |
handoffMode | ConnectHandoffMode | How openConnect sends people to Connect - ConnectProviderProps.handoff, after defaulting. |
session | ConnectSession | null | - |
isLoading | boolean | true until the first /session read settles. Render skeletons, not a Login button. |
error | string | null | - |
Methods
openConnect()
openConnect(target: string, options?: OpenConnectOptions): Window | null;Sends the visitor to a Connect page for a task, and brings them back when
it is done. Call it from a press handler - a popup opened after an
await is what pop-up blockers exist to stop.
target is a path ("/billing") or an absolute Connect URL, such as a
readiness blocker's resolveUrl. Anything on another origin throws.
"popup"mode opens Connect in a new tab (or a sized window, withfeatures), which closes itself when the task is done and tells this page first - every Connect component on it then re-reads. Returns the window, for a caller that wants to watch it."redirect"mode, or a popup the browser blocked, navigates this tab to Connect, which navigates back with?connect_result=. Returnsnull; the page is going away.
Either way the way back carries this page's URL and this application's
client_id, and Connect only honours an origin registered for that
client - register your app's origin among its redirect URIs.
Parameters
| Parameter | Type |
|---|---|
target | string |
options? | OpenConnectOptions |
Returns
Window | null
signIn()
signIn(options?: {
prompt?: "create";
}): void;Parameters
| Parameter | Type |
|---|---|
options? | { prompt?: "create"; } |
options.prompt? | "create" |
Returns
void
signOut()
signOut(): Promise<void>;Returns
Promise<void>
refresh()
refresh(): Promise<void>;Returns
Promise<void>
fetchResource()
fetchResource<T>(path: string): Promise<T>;GET through the bridge, relative to basePath. Shared cache, deduped in flight.
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
Promise<T>
mutateResource()
mutateResource<T>(
path: string,
body?: unknown,
method?: "POST" | "DELETE"): Promise<T>;POST through the bridge, relative to basePath. Unlike signOut
(which deliberately swallows a failed logout so the local UI doesn't
hang on a Connect outage), this throws on a non-2xx response - a caller
mutating state needs to know when it didn't take.
Evicts only path's own cache entry, not the whole shared cache - a
blunt full clear would break fetchResource's in-flight dedup for
every other resource mounted on the same page. A component that reads
path through useConnectResource still needs to call that hook's own
refetch() afterward; clearing the cache entry alone does not re-render
anything by itself.
method widens this to the one other verb the bridge answers, DELETE,
for the routes where removal is the whole operation and a body would be
ceremony - Ask AI's history is the first. It stays a two-value union
rather than string: everything else on this bridge is a POST, and a
free-form verb here would be an invitation to invent REST semantics the
bridge's exact-match route table does not have.
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
path | string |
body? | unknown |
method? | "POST" | "DELETE" |
Returns
Promise<T>
invalidateResource()
invalidateResource(path: string): void;Drops one path's cached read so the next fetchResource for it goes to
the bridge again.
useConnectResource's own refetch() is not enough on its own: it bumps
a version counter to re-run that hook's effect, and the effect calls
fetchResource, which hands back the same cached promise. That is
correct after a mutateResource (which evicts the entry itself), but a
caller polling for a change made somewhere else entirely - a checkout
completed in a Connect tab, say - has no mutation to piggyback on. So
invalidate, then refetch.
Scoped to one path for the same reason mutateResource is: clearing the
whole cache would break in-flight dedup for every other resource on the
page.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
void
streamResource()
streamResource(
path: string,
body: unknown,
options?: {
signal?: AbortSignal;
}): Promise<Response>;POST through the bridge and hand back the raw Response, unread and
uncached - the seam <ConnectAskAi> streams server-sent events over.
Separate from mutateResource rather than a flag on it, because the two
differ in every respect that matters: this one must not await response.json() (that buffers the whole stream and defeats the point),
must not touch the resource cache (a conversation is not a resource), and
takes an AbortSignal so a "Stop" button can actually stop generation.
Throws on a non-2xx, having read the error envelope - which is safe, because a failure never has a stream behind it.
Parameters
| Parameter | Type |
|---|---|
path | string |
body | unknown |
options? | { signal?: AbortSignal; } |
options.signal? | AbortSignal |
Returns
Promise<Response>
readOneTapHint()
readOneTapHint(): Promise<ConnectOneTapHint | null>;Asks Connect whether this visitor is already signed in there, without navigating anywhere.
This is the read <ConnectOneTap> is built on, and the reason One Tap
beats silentSignIn: one credentialed fetch answers the question that
used to cost a full-page redirect to Connect and back.
It only works when the application is served from the same site as
Connect - a cosmoops.com subdomain. SameSite is judged on the
registrable domain, so the visitor's Connect session cookie rides along
from crm.cosmoops.com and does not from anywhere else. Off-site
deployments get signed_out, which is why every caller treats that answer
as "say nothing" rather than "this visitor has no account".
Resolves to null when the bridge offers no One Tap endpoints, or when the
request itself fails - a Connect that cannot be reached says nothing about
whether anyone is signed into it.
Returns
Promise<ConnectOneTapHint | null>
oneTapSignIn()
oneTapSignIn(): Promise<
| {
ok: true;
}
| {
ok: false;
reason: ConnectOneTapFailure;
}>;Completes a One Tap sign-in, with no navigation at any point.
Three requests: the bridge mints a PKCE pair and keeps the verifier, Connect hands back an authorization code, the bridge exchanges it and stores the session. The page only ever holds the code, which is worthless without the verifier it never sees.
Refreshes the session on success, so consumers re-render signed in.
Returns
Promise<
| {
ok: true;
}
| {
ok: false;
reason: ConnectOneTapFailure;
}>
Interface: ConnectCreateWorkspaceModalProps
Properties
| Property | Type | Description |
|---|---|---|
isOpen | boolean | - |
onOpenChange | (isOpen: boolean) => void | - |
onCreated? | (workspace: UserWorkspace) => void | Called once the workspace is created, before the modal closes. |
Interface: ConnectCursorPaginationProps
Properties
| Property | Type | Description |
|---|---|---|
shown | number | How many rows the current page is showing. Used for the live-region count. |
nextCursor | string | null | The cursor for the next page, or null when this is the last one. |
hasPrevious | boolean | Whether a previous page exists - the caller owns the cursor history. |
onNext | (cursor: string) => void | - |
onPrevious | () => void | - |
isPending? | boolean | - |
className? | string | Merged onto the root, so a utility here replaces the conflicting default rather than sitting beside it. See cn. |
noun? | readonly [string, string] | Singular/plural noun for the count, e.g. ["contact", "contacts"]. |
Interface: ConnectEntitlements
Properties
| Property | Type |
|---|---|
readiness | ReadinessResponse |
subscription | SubscriptionResponse |
Interface: ConnectEventTypeFormModalProps
Properties
| Property | Type | Description |
|---|---|---|
isOpen | boolean | - |
onOpenChange | (isOpen: boolean) => void | - |
eventTypeId? | string | null | null (or omitted) creates a new appointment type. A row id from useManageableEventTypes edits that one instead - name, duration, buffers, host, schedule, location and publicBookingEnabled all prefill from it. |
onSaved? | (eventType: ManageableEventType) => void | Called once the appointment type is created or saved, before the modal closes. |
Interface: ConnectFeatureGateProps
Properties
| Property | Type | Description |
|---|---|---|
feature | string | The feature key, as it appears in plan.features. |
children | ReactNode | - |
fallback? | ReactNode | Rendered when the plan does not include feature. Defaults to an upsell card pointing at your plans page - pass null to hide the module silently instead. |
loading? | ReactNode | Shown while the first read is in flight. Defaults to nothing. |
plansHref? | string | Where your plan catalogue is mounted, for the default upsell. |
title? | string | Headline for the default upsell - name the feature, not the plan. |
description? | string | - |
className? | string | - |
Interface: ConnectFilterTabsOption<TValue>
Type Parameters
| Type Parameter |
|---|
TValue extends string |
Properties
| Property | Type | Description |
|---|---|---|
value | TValue | null | - |
label | string | - |
count? | number | Shown as a count beside the label. Omit where a count would need its own query. |
Interface: ConnectFilterTabsProps<TValue>
Type Parameters
| Type Parameter |
|---|
TValue extends string |
Properties
| Property | Type | Description |
|---|---|---|
options | readonly ConnectFilterTabsOption<TValue>[] | - |
value | TValue | null | - |
onChange | (value: TValue | null) => void | - |
className? | string | Merged onto the root, so a utility here replaces the conflicting default rather than sitting beside it. See cn. |
label | string | Names the group for assistive technology - "Filter contacts by lifecycle". |
Interface: ConnectGrievanceNoticeProps
Properties
| Property | Type | Description |
|---|---|---|
title? | ReactNode | Heading text. null renders none, for embedding inside a page that already has one. |
applicationName? | string | What this application calls itself in the sentence naming the processor relationship. Omit to leave that sentence out entirely. |
className? | string | - |
Interface: ConnectIntegrationsModalProps
Properties
| Property | Type | Description |
|---|---|---|
className? | string | Extra classes on the trigger button. |
label? | string | Trigger label, both visible and for screen readers. |
Interface: ConnectLogoProps
Properties
| Property | Type | Description |
|---|---|---|
name | string | This application's name - rendered as the sub-brand beneath the cosmo·ops wordmark, in the house cosmo·ops VOICE treatment. |
icon? | ReactNode | This application's own glyph, drawn inside the accent badge. Any node - a lucide-react icon, an <img>, an inline <svg>. Paint it with currentColor and it inherits --accent-foreground from the badge, so it stays legible when a host app changes the accent. Falls back to the Connect rings when omitted. |
variant? | "mark" | "wordmark" | "full" | Which halves of the lockup to draw. - "full" - badge and wordmark, the default. - "mark" - the badge alone, for a collapsed rail or a favicon-sized slot. - "wordmark" - the words alone, for a layout that already draws the badge somewhere fixed. The sidebar header uses this: the badge stays put while the words fade, and rendering a second badge only to hide it with a CSS child selector is how that kind of layout quietly rots. |
collapsed? | boolean | Mark only, no wordmark. Deprecated Pass variant="mark". Kept because it is the published 0.11 API; variant wins when both are given. |
className? | string | - |
Interface: ConnectNavItem
One destination in the shell's sidebar.
href is a path in your app, not a Connect URL - the shell never
routes anywhere itself, it hands href to renderLink and lets your
router do the work.
Properties
| Property | Type | Description |
|---|---|---|
label | string | - |
href | string | - |
icon? | ReactNode | A pre-rendered glyph, e.g. <LayoutDashboard className="size-5" />. Required in practice for anything in the primary rail: the collapsed rail is icon-only, and an item with no icon collapses to an empty box. |
badge? | ReactNode | Trailing content on the row - a count <Chip>, a status dot. Hidden while collapsed. |
exact? | boolean | Match this route only when the path is exactly href, never as a prefix. / is always treated as exact regardless, since every path starts with it. |
external? | boolean | Opens in a new tab, with a trailing arrow. For docs, status pages, anything outside the app. |
items? | ConnectNavItem[] | One level of children, revealed beneath the parent when the parent or a child is active. |
gateExempt? | boolean | Leave this route outside ConnectAccessGate - see AppShellProps.gate. Declared here, next to the route it applies to, rather than as a second list somewhere else that has to be kept in step with this one. |
audience? | "member" | "staff" | Who this route is for. Omit - the default - and it renders for everyone, which is what every route on an ordinary application is. - "staff" shows it only to a Connect platform staff session; your operator console goes here. - "member" hides it from staff. Reach for it on anything that reads the workspace - billing, plans, usage, team - since a staff session has no workspace and those routes can only fail for it. Filtering here is presentation, not access: it decides which links a person sees, and a link is not a permission. The route itself must still check staff on your server - see <ConnectPlatformGate>. |
Interface: ConnectNavLinkProps
Props the shell hands to renderLink. Spread them onto whatever your router's link component is.
Properties
| Property | Type | Description |
|---|---|---|
href | string | - |
className | string | - |
children | ReactNode | - |
onClick? | () => void | - |
aria-current? | "page" | - |
aria-label | string | Always set, and the only name the link has while the rail is collapsed - spread it through rather than dropping it, or an icon-only rail reads as a column of unlabelled links. |
target? | string | - |
rel? | string | - |
Interface: ConnectNavSection
A titled group of items. Untitled groups render as a plain divider-free block.
Properties
| Property | Type |
|---|---|
title? | string |
items | ConnectNavItem[] |
Interface: ConnectNoSubscriptionCardProps
Properties
| Property | Type | Description |
|---|---|---|
plansHref? | string | Where your plan catalogue is mounted. Defaults to /plans. |
title? | string | Override the headline - "Upgrade to unlock reports", say, rather than the generic one. |
description? | string | - |
className? | string | - |
Interface: ConnectOneTapHint
What Connect says about the visitor, before anything has been signed.
Note what is not here when consented is false: no name, no email, no
photo. Connect withholds all of it until the visitor has authorised this
application at least once, so the first prompt an application ever shows is
necessarily the impersonal one.
Properties
| Property | Type |
|---|---|
status | "signed_out" | "signed_in" |
consented? | boolean |
user? | { displayName: string | null; email: string | null; photoUrl: string | null; } |
user.displayName | string | null |
user.email | string | null |
user.photoUrl | string | null |
workspace? | | { name: string; } | null |
application? | { name: string; iconUrl: string | null; } |
application.name | string |
application.iconUrl | string | null |
Interface: ConnectOneTapProps
"Continue as …" - a signed-out visitor who is already signed into Connect is offered their account, and one tap signs them in without leaving the page.
The prompt is the visible half; what makes it worth having is that finding
out costs one fetch rather than a redirect. ConnectProviderProps.silentSignIn
answers the same question by bouncing the browser to Connect and back, which
means a visible full-page navigation on the first load of every signed-out
visit, nothing shown to the visitor while it happens, and no way to say no.
<ConnectProvider>
<App />
<ConnectOneTap />
</ConnectProvider>It only appears when it can actually help
Nothing renders unless the visitor is signed out here, signed in to Connect,
and has not recently dismissed the prompt or signed out of this application.
A visitor with no Connect session sees nothing at all, ever - which is why
this is safe to mount unconditionally, unlike silentSignIn, whose cost
lands on exactly the people it cannot help.
Two prompts, because consent is not assumed
A visitor who has authorised this application before gets their name, email, workspace and photo, because Connect will release those to an application they have already agreed to. A visitor who has not gets an impersonal prompt and a tap that leads to the real consent screen - Connect does not hand over so much as a first name until that has happened once.
Placement
Bottom of the viewport on a phone, bottom-right from sm: up. Deliberately
not top-right: on a phone that is where the browser chrome and the
application's own header live, and this must never cover the thing the
visitor came to use.
Properties
| Property | Type | Description |
|---|---|---|
onInteractiveSignIn? | () => void | Where to send someone whose tap needs the interactive screen - the first connection to this application, or an account that has an MFA or subscription step outstanding. Defaults to the bridge's own /signin. |
className? | string | - |
Interface: ConnectPlansPageProps
Extends
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
title? | string | - | - |
description? | string | - | - |
showCurrentSubscription? | boolean | Show the current subscription above the grid. On by default - someone arriving at a pricing page while already subscribed is usually here to change something, and the first thing they need is what they are on now. | - |
planIds? | string[] | Restrict the grid to these plan ids, in this order. Defaults to every active plan. | ConnectPlansProps.planIds |
hideCoupon? | boolean | Hide the coupon field. | ConnectPlansProps.hideCoupon |
className? | string | - | ConnectPlansProps.className |
Interface: ConnectPlansProps
Extended by
Properties
| Property | Type | Description |
|---|---|---|
planIds? | string[] | Restrict the grid to these plan ids, in this order. Defaults to every active plan. |
hideCoupon? | boolean | Hide the coupon field. |
className? | string | - |
Interface: ConnectPlatformGateProps
Properties
| Property | Type | Description |
|---|---|---|
roles? | string[] | Restrict to these platform roles. Omit to accept any staff session, which is the common case - your application already chose which roles it accepts when a platform admin turned staff access on for it, and repeating that list here is a second copy to keep in sync. Pass one only when your own console has tiers: a developer seeing the read-only half of what a platform_admin can edit. |
children | ReactNode | - |
fallback? | ReactNode | Rendered for an ordinary member session. Defaults to null - hiding the subtree, which is what this gate is normally for. An operator console is usually better hidden than explained. |
loading? | ReactNode | Shown until the session read settles. Defaults to nothing. |
Interface: ConnectProfileModalProps
Properties
| Property | Type | Description |
|---|---|---|
isOpen | boolean | Controlled open state. |
onOpenChange | (isOpen: boolean) => void | - |
onSignOut? | () => void | Called after sign-out completes, e.g. to route home. |
theme? | ThemeMode | See ConnectUserButtonProps.theme. |
onThemeChange? | (theme: ThemeMode) => void | - |
Interface: ConnectProviderProps
Properties
| Property | Type | Description |
|---|---|---|
clientId? | string | This application's public client_id, from its Connect client section in Connect. Not needed for the default sign-in flow: signIn() with no redirectUri bounces the browser to your own /signin route, which already knows client_id server-side (CONNECT_CLIENT_ID) and builds the authorize URL itself - the browser never needs it. Pass this only alongside an explicit redirectUri, when you're building that URL yourself instead. |
basePath? | string | Where createConnectHandler is mounted in your app. Must match its basePath. |
redirectUri? | string | Where Connect sends the user back after sign-in. Your own callback route, not a Connect URL - it is your backend that exchanges the code. |
appUrl? | string | Override for non-production Connect instances. Defaults to https://connect.cosmoops.com. The twin of CONNECT_APP_URL, and not derived from it - that variable is read on your server and never reaches the browser. This value is where the user menu's Profile, Billing and Invoices destinations open, where ConnectAppsGrid sends an unsubscribed application, where checkout opens, and - if you pass redirectUri and so build the authorize URL here rather than in your own /signin route - where sign-in goes. Point the server somewhere else and leave this alone and the two halves disagree, so pass both or neither. Your root layout is a Server Component, so appUrl={process.env.CONNECT_APP_URL} is enough and needs no browser-visible variable. |
fetch? | (input: string, init?: RequestInit) => Promise<Response> | Replace the fetch used to reach your bridge. The browser-side twin of ConnectClientOptions.fetch - it exists so Storybook and tests can drive the real provider against canned responses instead of monkey-patching window.fetch, which is a render-time global mutation and races with child effects. |
silentSignIn? | boolean | Sign in a returning visitor automatically, with no click. When the first /session read comes back signed out, the browser is sent on one round trip to Connect with OIDC prompt=none. If they still have a Connect session and have authorised this app before, they come straight back signed in and never see a screen. If not, they come straight back signed out, with no error and nothing rendered differently - your normal signed-out UI just appears a beat later. Off by default, and it is a real trade, not just caution. It costs a full-page redirect on the first load of every signed-out visit - a brief flash on a marketing page, invisible on an app shell - so it belongs on apps whose visitors are overwhelmingly already Connect users. It is attempted at most once per tab (a sessionStorage marker), so a visitor who genuinely has no session does not bounce on every navigation. Requires createConnectHandler mounted at basePath; the redirect goes to your own /signin, which mints the state server-side. |
handoff? | ConnectHandoffMode | How every Connect button in this package - Billing, Profile, Manage, checkout, a readiness blocker's fix - takes the visitor to Connect. - "popup" (the default): Connect opens in a new tab and closes itself when the task is done, telling this page first, so the visitor is back where they were with everything repainted. A blocked popup falls back to "redirect" rather than leaving them with nothing. - "redirect": this tab goes to Connect and comes back. Prefer it for an installed PWA or an app inside an iframe, where a second tab is a different window the visitor may never find. Both need this app's origin registered among the client's redirect URIs in Connect - an unregistered origin still reaches Connect, it just gets no way back. |
onHandoffResult? | (result: ConnectHandoffResult) => void | Told how each trip to Connect ended - from the popup's message, or from ?connect_result= on the way back from a redirect (which the provider then strips from the address bar). The provider already re-reads the session and every Connect resource; this is for your own state. |
children | ReactNode | - |
Interface: ConnectReadinessBannerProps
Properties
| Property | Type | Description |
|---|---|---|
includeAdvisory? | boolean | Show advisory blockers too. Off by default - they are not a reason to interrupt anyone mid-task. |
hideWhenReady? | boolean | Render nothing at all once the workspace is ready. On by default. |
className? | string | - |
Interface: ConnectReadinessGateProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | - |
fallback? | ReactNode | Rendered instead of children while blocked. Defaults to the banner in a card. |
loading? | ReactNode | Shown while the first readiness read is in flight. |
className? | string | - |
Interface: ConnectSession
Properties
| Property | Type | Description |
|---|---|---|
user | ConnectUser | null | - |
subscription | | SessionSubscription | null | - |
role | string | null | The signed-in account's role on this application, or null - see SessionAccess.role. null when signed out too, which is why user rather than this is the signed-in test. |
permissions | string[] | What to gate on - see SessionAccess.permissions. [] when signed out. |
staff | boolean | Whether this is one of Connect's own operators - see SessionAccess.staff. false when signed out. |
audience | SessionAudience | null | null when signed out, for the same reason user and subscription are: there is no workspace to compare an audience against yet. Never null on a signed-in session. |
oneTap? | | { hintUrl: string; authorizeUrl: string; } | null | Where the browser may ask Connect about the visitor, or null when this deployment cannot run One Tap - see IdentityResource.oneTapEndpoints. Delivered on the session read the provider already makes, so discovering One Tap costs no extra request. Optional because an older bridge will not send it. |
clientId? | string | null | This application's public client_id, from the bridge - so openConnect can name the application on a hand-off without clientId being passed to <ConnectProvider> as well. Optional because an older bridge will not send it; the provider's own clientId prop wins when both exist. |
Interface: ConnectShellContextValue
Properties
| Property | Type | Description |
|---|---|---|
collapsed | boolean | Icon-only rail. Always false inside the mobile drawer - see AppShell. |
collapsible | boolean | Whether a collapse control should be offered at all. |
setCollapsed | (collapsed: boolean) => void | - |
renderLink | ConnectLinkRenderer | - |
activePath? | string | The host router's current path, if it supplied one. |
onNavigate? | () => void | Called after any sidebar link is pressed - the mobile drawer closes itself with this. |
Interface: ConnectSidebarHeaderProps
Properties
| Property | Type | Description |
|---|---|---|
name | string | This application's name, in the cosmo·ops VOICE lockup. See ConnectLogo. |
icon? | ReactNode | This application's glyph, drawn on the CosmoOps badge. |
homeHref? | string | Where the lockup links when the rail is expanded. Defaults to /. |
Interface: ConnectSidebarNavProps
Properties
| Property | Type | Description |
|---|---|---|
nav | ConnectNav | Items, sections, or a mix - see ConnectNav. |
activePath? | string | The current path. Pass your router's own (usePathname() in Next). Inherited from AppShell when omitted; without either, the nav reads window.location and patches history to hear about client-side navigations - which works, but is guesswork the router never has to do. See useActivePath. |
renderLink? | ConnectLinkRenderer | See ConnectLinkRenderer. Inherited from the shell; defaults to a plain <a>. |
collapsed? | boolean | Icon-only rail. Inherited from the shell when omitted. |
onNavigate? | () => void | Called after any nav link is pressed - the mobile drawer closes itself with this. |
className? | string | - |
Interface: ConnectSidebarProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | The rail's contents, top to bottom. AppShell composes the default set; pass your own to rearrange them. |
label? | string | Accessible name for the <aside>. Defaults to "Sidebar". |
className? | string | - |
Interface: ConnectSubscribeDialogProps
Properties
| Property | Type | Description |
|---|---|---|
plan | Plan | null | null closes it. The plan being subscribed to or moved to. |
action | PlanAction | Which of subscribe / upgrade / downgrade this is - decides the headline and what the notice says about money. |
current | | SubscriptionResponse | null | The workspace's live subscription, for the "from" side of a plan change. |
coupon? | string | Coupon typed into the plan grid, echoed here so it is visible before the hand-off carries it. |
Methods
onClose()
onClose(): void;Whether this workspace's plan.trialDays still applies is Connect's call, not this component's - see the notice below.
Returns
void
onConfirm()
onConfirm(): void;Returns
void
Interface: ConnectSubscriptionCardProps
Properties
| Property | Type |
|---|---|
plansHref? | string |
className? | string |
Interface: ConnectSubscriptionGateProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | - |
plansHref? | string | Where your plan catalogue is mounted. Defaults to /plans. |
fallback? | ReactNode | Replaces the default "not on a plan yet" card. |
loading? | ReactNode | Shown while the first read is in flight. Defaults to a skeleton, never a fallback - see below. |
allowTrialing? | boolean | Let a trialing subscription through. On by default: a trial is a subscription, and gating it out is how a customer gets sold a trial and then locked out of it. |
allowPending? | boolean | Let a subscription whose first debit hasn't confirmed through. On by default - the charge settles on a webhook and can take minutes, and the alternative is telling someone who just paid that they haven't. |
className? | string | - |
Interface: ConnectTrialBannerProps
Properties
| Property | Type |
|---|---|
plansHref? | string |
className? | string |
Interface: ConnectUsageMeterProps
Properties
| Property | Type | Description |
|---|---|---|
addonIds? | string[] | Restrict to these add-on ids, in this order. Defaults to every metered add-on. |
hideWhenEmpty? | boolean | Render nothing when there is nothing to meter, instead of an empty-state card. On by default. |
title? | string | - |
className? | string | - |
Interface: ConnectUserButtonProps
Properties
| Property | Type | Description |
|---|---|---|
variant? | "inline" | "row" | "compact" | How the trigger is drawn. - "row" - a full-width sidebar row: avatar, name over email, and a trailing glyph. The default, because the account control now lives in the rail's footer rather than in a top bar. - "compact" - the avatar alone, for a dense strip. - "inline" - avatar and name side by side, for a bar with room. |
onSignOut? | () => void | Called after sign-out completes, e.g. to route home. |
className? | string | - |
theme? | ThemeMode | Current theme, and where to send a change - the SDK has no theme provider of its own (no next-themes dependency), so this is entirely host-controlled. The Appearance section only renders when both are supplied; omit either to leave theme control out of the modal. |
onThemeChange? | (theme: ThemeMode) => void | - |
Interface: ConnectUserMenuProps
Properties
| Property | Type | Description |
|---|---|---|
compact? | boolean | Hide the name beside the avatar - for a dense top bar. |
onSignOut? | () => void | Called after sign-out completes, e.g. to route home. |
className? | string | - |
theme? | ThemeMode | Current theme, and where to send a change - the SDK has no theme provider of its own (no next-themes dependency), so this is entirely host-controlled. The Appearance section only renders when both are supplied; omit either to leave theme control out of the modal. |
onThemeChange? | (theme: ThemeMode) => void | - |
Interface: ConnectVisitorChatProps
Properties
| Property | Type | Description |
|---|---|---|
workspaceId | string | The customer whose website this widget is on. Required, and it is the one field that decides whose published knowledge the assistant answers from and whose diary it books into. Your bridge checks it against your own subscriptions before it reaches Connect. |
assistantName? | string | What the assistant calls itself, and the copy shown before anybody types. Yours, not Connect's. These never reach the model and are rendered from your own store, which is what lets the bubble paint its first frame with no network call at all. Connect holds what it enforces; you hold what you draw. |
greeting? | string | - |
suggestions? | readonly string[] | Opening questions offered as buttons. Three is what fits above a phone keyboard. |
bookingEventTypeSlug? | string | null | A published event type slug in that workspace, to offer a call. Omit for no booking. |
basePath? | string | Where createConnectHandler is mounted. Defaults to /api/connect, which is its own default. Passed explicitly here, rather than only inherited from ConnectProvider, because this component is built to work on a page that has no provider - see the component's own note. |
page? | string | The page the visitor is on, passed to Connect as context. Left unset it uses location.pathname. Pass something more meaningful - a page title, a product slug - where the path is not descriptive. |
title? | string | The bubble's accessible label, and the panel's heading. |
defaultOpen? | boolean | Start open. The panel owns its state after that. |
showWhenSignedIn? | boolean | Show the bubble even to a signed-in user. Off by default. The default is the useful one: a signed-in member has your product's own support surfaces and, if you mount it, <ConnectAskAi> - which is grounded in their actual workspace and can therefore answer things this assistant cannot. Two bubbles in the same corner is also the one layout bug this component could cause, since <ConnectAskAi> sits at the same z-30 bottom-right position. Turn it on for a marketing site that is a separate surface from the application, where "signed in" says nothing about what the visitor is reading. |
fetch? | (input: string, init?: RequestInit) => Promise<Response> | Replaces fetch, for a mount with no ConnectProvider around it. Inside a provider this is not needed and not read: the component goes through mutateResource/streamResource, which already use whatever fetch that provider was given - so Storybook and the docs site work by wrapping it, with nothing passed here. |
Interface: ConnectWorkspaceSwitcherProps
Properties
| Property | Type | Description |
|---|---|---|
onSwitched? | (workspace: UserWorkspace) => void | Called after a successful switch. Defaults to a full reload, which is almost always right: every server-rendered page in the host application was built for the previous workspace, and a client-side refresh would leave stale data on screen next to the new workspace's name. |
row? | boolean | Draw the trigger as a sidebar row rather than a plain button. On by default inside AppShell, where this sits directly under the brand; pass false for a standalone placement in your own chrome. |
className? | string | - |
alwaysShow? | boolean | Render even when the signed-in account has fewer than two workspaces. Off by default, which keeps this component's documented posture for every other host: an account with one workspace sees nothing, because a switcher offering one option teaches it nothing. Pass true on an application that wants a single-workspace account to be able to discover - and open - a second one without leaving its own chrome. Turning this on adds a "Create workspace" row that opens <ConnectCreateWorkspaceModal> inline: name, individual-or-business, and the tenant's own plan decides whether another workspace is allowed at all - the same guards Connect's own "Create workspace" button runs through, surfaced as the server wrote them rather than pre-checked here. |
Interface: ConnectWorkspaceTypeGateProps
Properties
| Property | Type | Description |
|---|---|---|
type | | WorkspaceType | WorkspaceType[] | The workspace type this subtree is for. Pass an array to accept either - which is the same as not using the gate at all, so it exists only to keep a type={someVariable} call site from having to special-case "both". |
children | ReactNode | - |
fallback? | ReactNode | Rendered for a workspace of the other type. Defaults to null - hiding the subtree, which is what this gate is normally for. |
loading? | ReactNode | Shown until the session read settles. Defaults to nothing. |
Interface: GridWindow
Properties
| Property | Type | Description |
|---|---|---|
startMinute | number | Minutes since local midnight the grid starts at - 480 is 08:00. |
endMinute | number | - |
Interface: OpenConnectOptions
Properties
| Property | Type | Description |
|---|---|---|
features? | string | window.open features for a sized pop-up window instead of a tab - checkout uses "popup,width=520,height=780". Must not include noopener: the window handle is how Connect reports back and closes. |
name? | string | window.open target name. A second openConnect with the same name reuses that window. |
Interface: PlanTier
One tier of the catalog: the product, and whichever cadences it is sold at.
Properties
| Property | Type | Description |
|---|---|---|
planGroup | string | - |
name | string | - |
description | string | - |
monthly | Plan | null | - |
annual | Plan | null | - |
oneTime | Plan | null | A plan paid once, never on a cadence - its own slot rather than falling into annual. A one-time plan has no renewal to discount against, so filing it as "annual" used to let a tier mixing a recurring plan with a one-time one compute a annualSavingPercent between a monthly charge and a lump sum, and offered the one-time purchase under the annual toggle as though it renewed. pickTierPlan in plans.tsx is the only reader that needs to know this slot exists. |
annualSavingPercent | number | null | Whole-percent saving from paying annually, or null when this tier has no pair to compare. |
Interface: PositionedBooking
Properties
| Property | Type | Description |
|---|---|---|
booking | Booking | - |
dateKey | string | - |
top | number | Fraction of the visible day, 0-1, from the top of the grid. |
height | number | Fraction of the visible day. Never 0 - see MIN_VISIBLE_FRACTION. |
column | number | Column index within its overlap cluster, and how many columns that cluster has. |
columns | number | - |
clipped | boolean | True when the appointment extends past the grid's visible window. |
Interface: SlotQuery
Properties
| Property | Type | Description |
|---|---|---|
eventTypeId | string | - |
from | Date | - |
to | Date | - |
excludeBookingId? | string | Set to the booking being moved. Without it the appointment counts as blocking its own replacement and a reschedule picker comes back empty for the one adjustment people actually make. |
Interface: StartCheckoutOptions
Properties
| Property | Type | Description |
|---|---|---|
url | string | Absolute Connect URL to open. Build it with buildCheckoutUrl. |
Methods
isSettled()
isSettled(entitlements: ConnectEntitlements): boolean;true once /entitlements reflects the change being waited on.
Compared against the entitlements read before the window opened, so a predicate is genuinely needed - "the subscription exists" is the right test for a first subscribe and the wrong one for a plan change, where a subscription existed all along.
Parameters
| Parameter | Type |
|---|---|
entitlements | ConnectEntitlements |
Returns
boolean
Interface: UseConnectPush
Properties
| Property | Type | Description |
|---|---|---|
status | ConnectPushStatus | - |
isEnabling | boolean | True while enable() is in flight. |
error | string | null | The last failure, in words a person can act on. Cleared by the next attempt. |
enable | () => Promise<string | null> | Ask for permission, register this browser with Connect, and resolve the device id - or null if the member declined or anything refused. Must be called from a user gesture: browsers refuse Notification.requestPermission() outside one, and the refusal is indistinguishable from the member saying no. |
disable | (deviceId: string) => Promise<void> | Stop notifications on one browser, by the id enable() returned. |
Interface: UseConnectResourceOptions
Options for useConnectResource.
Properties
| Property | Type | Description |
|---|---|---|
requiresSession? | boolean | Whether the read has to wait for a session. true by default, and right for every route but one. Pass false only for a bridge route that takes neither a session nor a workspace - today that is GET /company-profile alone, and its handler says why. Passing it for anything else does not widen access (the bridge still refuses), it just buys a guaranteed 401 sooner. |
Interface: VisitorStreamHandlers
The visitor turn's wire format, read.
A named-event reader, where <ConnectAskAi>'s is data-only. That one
consumes the AI SDK's UI message stream, where every frame is a data: line
carrying a type field. This endpoint answers event:/data: pairs instead,
for the reason the route explains: the UI message protocol exists for
useChat, which owns the message list client-side, and this surface
deliberately does not let the client own the transcript.
Kept out of the component and pure so it can be tested without a DOM. The
three behaviours worth pinning are all here: a frame split across two network
reads, a malformed frame, and a stream that ends without its done.
Methods
onDelta()
onDelta(text: string): void;Parameters
| Parameter | Type |
|---|---|
text | string |
Returns
void
onDone()
onDone(done: VisitorTurnDone): void;Parameters
| Parameter | Type |
|---|---|
done | VisitorTurnDone |
Returns
void
onError()
onError(message: string): void;A failure the server reported after the stream opened.
It arrives as an event rather than a status because the status was already sent, and the message is written as a sentence for the same reason - it is shown to the visitor verbatim.
Parameters
| Parameter | Type |
|---|---|
message | string |
Returns
void
Interface: WorkspaceActionsState
Properties
| Property | Type | Description |
|---|---|---|
isPending | boolean | - |
error | string | null | The last refusal, written to be shown to the person who clicked - see CreateWorkspaceUseCase. |
Methods
clearError()
clearError(): void;Returns
void
Type Aliases
Type Alias: AiStatusTone
type AiStatusTone = "neutral" | "positive" | "warning" | "danger";Type Alias: ConnectAccessGateState
type ConnectAccessGateState =
| "loading"
| "signed_out"
| "suspended"
| "audience_mismatch"
| "no_subscription"
| "onboarding_pending"
| "blocked"
| "payment_retry"
| "clear";Type Alias: ConnectCheckoutStatus
type ConnectCheckoutStatus = "idle" | "waiting" | "completed" | "abandoned" | "blocked";Where a hand-off to Connect has got to.
idle- nothing started, or the last one was acknowledged.waiting- Connect is open and this hook is polling/entitlements.completed- the entitlement actually changed. The only success signal worth trusting; see this module's doc comment for why the Connect window closing is not one.abandoned- the window closed (or the deadline passed) with the entitlement unchanged.blocked- no longer produced. A blocked window now falls back to a same-tab trip to Connect and back (seeopenConnect), which reaches the same place without asking the customer to click a fallback link. Kept in the union so code that handles it still compiles.
Type Alias: ConnectHandoffMode
type ConnectHandoffMode = "popup" | "redirect";How this application sends people to Connect - see ConnectProviderProps.handoff.
Type Alias: ConnectHandoffResult
type ConnectHandoffResult = "completed" | "cancelled" | "closed";How a trip to Connect ended, as Connect reports it.
Advisory. A payment confirmed by webhook can settle a moment after the
window has closed, so "completed" means "they finished there", not "it is
live" - re-read entitlements for that. The provider already does, for every
hand-off.
Type Alias: ConnectLinkButtonProps
type ConnectLinkButtonProps = Omit<ButtonProps, "render"> & {
href: string;
renderLink?: (props: ConnectLinkButtonRenderProps) => ReactElement;
};Type Declaration
href
href: string;renderLink?
optional renderLink?: (props: ConnectLinkButtonRenderProps) => ReactElement;The host's router link, if it has one.
Without it this renders a plain <a href>, which navigates under every
router but costs a full document load - in an App Router app that
throws away the client cache and re-runs the whole tree, which is very
visible on a button sitting inside a page the user is working in.
<ConnectLinkButton href="/deals" renderLink={(props) => <Link {...props} />}>
Open board
</ConnectLinkButton>Spread the props through whole - see ConnectLinkButtonRenderProps.
Same shape as AppShell's renderLink, so a host declares its router
once and passes the same function to both. This package imports nothing
from next - see the ./next entry's doc comment for why that is a hard
rule - so handing the link component in is the only way it can be a client
transition.
Parameters
| Parameter | Type |
|---|---|
props | ConnectLinkButtonRenderProps |
Returns
ReactElement
Type Alias: ConnectLinkButtonRenderProps
type ConnectLinkButtonRenderProps = Omit<ComponentPropsWithRef<"a">, "href" | "children"> & {
href: string;
children: ReactNode;
};What ConnectLinkButton hands its renderLink: everything a React Aria
Button puts on its element, already merged - the ref, the press,
keyboard, focus and hover handlers, id, the aria-* and data-*
attributes HeroUI styles from, className and children - retargeted at an
anchor. Spread all of it onto your link component; picking out href,
className and children is exactly what disconnects the ref and the press
handling.
Type Declaration
href
href: string;children
children: ReactNode;Type Alias: ConnectLinkRenderer
type ConnectLinkRenderer = (props: ConnectNavLinkProps) => ReactNode;How this app renders a link.
The SDK ships into any React app and cannot import next/link,
react-router or anything else framework-specific, so routing is a prop.
The default is a plain <a>, which is correct but costs a full page load;
an app with a client router should pass its own:
renderLink={(props) => <Link {...props} />} // next/linkParameters
| Parameter | Type |
|---|---|
props | ConnectNavLinkProps |
Returns
ReactNode
Type Alias: ConnectNav
type ConnectNav = ReadonlyArray<
| ConnectNavItem
| ConnectNavSection>;What AppShell/ConnectSideNav accept for nav: a flat list of
items, a list of sections, or a mix. Consecutive bare items fold into one
untitled section, so the common case is just an array of items.
Type Alias: ConnectOneTapFailure
type ConnectOneTapFailure =
| "unavailable"
| "login_required"
| "consent_required"
| "interaction_required"
| "rate_limited"
| "failed";Why a tap did not end in a session.
Every one of these is ordinary traffic rather than a fault, which is why
oneTapSignIn reports them as a value instead of throwing: consent_required
and interaction_required both mean "this needs the interactive screen", and
that is a hand-off, not an error to show anybody.
Type Alias: ConnectPushStatus
type ConnectPushStatus =
| "checking"
| "unsupported"
| "unconfigured"
| "denied"
| "prompt"
| "granted";Whether this browser can show notifications, and whether it has been asked.
Five states, not a boolean - and that is the whole reason this hook exists
rather than a <Switch>:
unsupported- no service worker or no Push API. On iOS this is every browser until the site is added to the home screen, which is worth saying out loud because it is most of the phones in some markets.unconfigured- the platform has no VAPID keys, so nothing could be delivered even if permission were granted.denied- and this one cannot be undone by script. A browser will not show the prompt again once told no; only the site-settings panel in the address bar can reverse it. A control that silently does nothing when pressed is the worst version of this screen, so the state is surfaced and the UI is expected to explain it.prompt- askable.granted- permission held. It does not follow that this browser is registered with Connect; permission and registration are separate facts, and a member who cleared site data holds the first without the second.
Type Alias: ConnectSidebarRowProps
type ConnectSidebarRowProps = Readonly<{
icon: ReactNode;
label: ReactNode;
aria-label?: string;
tile?: boolean;
trailing?: ReactNode;
}> & Omit<ComponentPropsWithoutRef<"button">, "aria-label">;Type Alias: ConnectSideNavProps
type ConnectSideNavProps = ConnectSidebarNavProps;Deprecated
Renamed to ConnectSidebarNavProps.
Type Alias: ConnectSubscriptionState
type ConnectSubscriptionState = "none" | "trialing" | "active" | "cancelling" | "pending" | "problem";What a subscription is actually doing, collapsed to the five cases worth rendering differently.
status alone cannot answer this. "active" covers both a paying customer
and one still in a trial (isTrialing on the session block is what splits
them), and it says nothing about a subscription winding down -
cancelAtPeriodEnd is a separate field precisely so a cancelling
subscription is not mistaken for a renewing one.
Type Alias: PlanAction
type PlanAction =
| "current"
| "upgrade"
| "downgrade"
| "switch-to-annual"
| "switch-to-monthly"
| "subscribe";What moving to plan would be, given what the workspace is on today.
switch-to-annual/switch-to-monthly exist because a change of cadence
is neither an upgrade nor a downgrade, and calling it one is not a wording
problem. Connect picks the use-case that runs from the same classification
(classifyPlanChange in packages/data), and a same-tier move to annual
compares cheaper per month - so it used to be sent down the downgrade
path, where the price delta across cadences is negative, floors to zero,
and settles the whole switch for no money at all.
Variables
Variable: ACTION_LABEL
const ACTION_LABEL: Record<PlanAction, string>;Variable: AI_EMPTY_VALUE
const AI_EMPTY_VALUE: "—" = "—";Renders in place of a missing value - an em dash, not the word "null".
Variable: AI_TONE_CHIP
const AI_TONE_CHIP: Record<AiStatusTone, string>;HeroUI semantic tokens only, so these inherit whatever theme the host defined.
Variable: AI_TONE_DOT
const AI_TONE_DOT: Record<AiStatusTone, string>;Variable: CONNECT_APP_URL
const CONNECT_APP_URL: "https://connect.cosmoops.com" = "https://connect.cosmoops.com";Browser half of @cosmoops/connect-sdk - React components and hooks.
Holds no credential. Everything here talks to your own origin, where
createConnectHandler from @cosmoops/connect-sdk/server holds the API key
and reaches Connect.
Styling is HeroUI v3 semantic tokens only (accent, muted, surface,
separator, ...), never a hardcoded colour - so these components inherit
whatever theme the host application platform already configured, and one theme
change lands identically in all of them.
Variable: CONNECT_HANDOFF_MESSAGE_TYPE
const CONNECT_HANDOFF_MESSAGE_TYPE: "connect:handoff" = "connect:handoff";MessageEvent.data.type of the message Connect posts before closing a popup.
Variable: CONNECT_HANDOFF_RESULT_PARAM
const CONNECT_HANDOFF_RESULT_PARAM: "connect_result" = "connect_result";The query param Connect adds when it redirects back.
Variable: ConnectSideNav
const ConnectSideNav: (__namedParameters: ConnectSidebarNavProps) => Element = ConnectSidebarNav;The sidebar's nav column on its own - sections, items, one level of children, and the active highlight.
AppShell renders this inside its rail and its mobile drawer, and it
is exported for a host that wants the shell's navigation without its chrome:
the same items, the same active resolution, dropped into a layout you
already have.
Every prop is optional except nav, because inside a shell they are all
inherited from it. Passing one wins over what the shell says - which is how
a second nav column (see secondaryNav) can share the rail's collapse state
while keeping its own items.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSidebarNavProps |
Returns
Element
Deprecated
Renamed to ConnectSidebarNav in 0.12 - the rail is a
ConnectSidebar now, and its pieces are named after it. Identical
behaviour; this alias will be removed in a later minor.
Variable: RAIL_COLLAPSED
const RAIL_COLLAPSED: 64 = 64;The rail's two widths, in pixels, and the one place they are written down.
They are numbers rather than Tailwind classes because the rail animates
between them - motion interpolates a number and cannot interpolate
w-16 into w-64.
The pairing is load-bearing, not arbitrary. Every row in the sidebar is
laid out so its glyph sits exactly RAIL_GUTTER + ROW_GUTTER + 10px from
the rail's left edge - 32px, which is also RAIL_COLLAPSED / 2. So the
collapsed rail centres every icon without a single centring rule, and
nothing moves horizontally while the width animates. Change either width
and that identity breaks: the icons drift sideways during the transition,
which is the exact stutter this geometry exists to avoid.
Variable: RAIL_EXPANDED
const RAIL_EXPANDED: 264 = 264;Functions
Function: AiChart()
function AiChart(__namedParameters: Readonly<AiChartInput>): Element;showChart. Cartesian kinds are drawn as SVG against a fixed viewBox and
scaled by the browser, which is why there is no resize observer here: the
chart is resolution-independent, so "responsive" costs nothing at runtime.
pie is deliberately not a pie. In a 400px column a pie plus its legend
is smaller than the labels it needs, and reading angles is worse than
reading lengths at any size - so proportions render as a labelled bar list,
which answers the same question and stays legible.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiChartInput> |
Returns
Element
Function: AiDetails()
function AiDetails(__namedParameters: Readonly<AiDetailsInput>): Element;showDetails: one entity's fields, as a definition list rather than a one-row table.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiDetailsInput> |
Returns
Element
Function: AiMetrics()
function AiMetrics(__namedParameters: Readonly<AiMetricsInput>): Element;showMetrics. A delta is coloured by deltaMeaning, not by its sign -
churn rising is bad and revenue rising is good, and painting both green
because the arrow points up is exactly the confidently-wrong signal a
dashboard must never send. The arrow still shows direction, so meaning and
direction stay two separate channels.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiMetricsInput> |
Returns
Element
Function: AiSources()
function AiSources(__namedParameters: Readonly<AiSourcesInput>): Element;showSources: what a grounded answer actually rested on. The point of a
citation is that it can be opened, so a source with a uri is a link and one
without is still a named row - never a marker with nothing behind it.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiSourcesInput> |
Returns
Element
Function: aiStatusTone()
function aiStatusTone(value: AiValue): AiStatusTone;Parameters
| Parameter | Type |
|---|---|
value | AiValue |
Returns
Function: AiSteps()
function AiSteps(__namedParameters: Readonly<AiStepsInput & {
renderDetail?: (markdown: string) => ReactNode;
}>): Element;showSteps: an ordered procedure. renderDetail is how the panel feeds each step back through its markdown renderer.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiStepsInput & { renderDetail?: (markdown: string) => ReactNode; }> |
Returns
Element
Function: AiTable()
function AiTable(__namedParameters: Readonly<AiTableInput>): Element;showTable. Sorting and CSV are what a table is for - once an answer is
tabular the next thing anyone wants is to reorder it or take it elsewhere,
and a markdown table can do neither. Both work off the raw values, so
"₹12,500" sorts as 12500 and the export carries numbers a spreadsheet can add.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiTableInput> |
Returns
Element
Function: AiTimeline()
function AiTimeline(__namedParameters: Readonly<AiTimelineInput>): Element;showTimeline: dated events, oldest first, on a single rail.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<AiTimelineInput> |
Returns
Element
Function: annualSaving()
function annualSaving(monthly: Plan | null | undefined, annual: Plan | null | undefined): number | null;Annual is quoted per year on the wire (billingInterval: "annual"), so a
side-by-side grid has to normalise before it can compare. Showing the saving
against 12x the monthly plan is the comparison a customer is actually making.
Call this with a matched pair from one tier. It used to be handed the
first monthly and the first annual plan in the whole response - two plans
that were the same tier only by the accident of document order, and that
produced one badge for a catalog where each tier discounts differently.
groupPlansByTier now does the pairing.
Parameters
Returns
number | null
Function: AppShell()
function AppShell(__namedParameters: AppShellProps): Element;The application frame every CosmoOps application platform runs inside: one rail down the side holding the brand, the workspace, the navigation and the account, and your pages in the rest.
This is the piece each partner was otherwise writing by hand, and writing slightly differently - which is how a family of applications stops looking like one product. Everything that should be identical across platforms lives here; everything that genuinely differs is a prop:
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { AppShell } from "@cosmoops/connect-sdk/client";
import { LayoutDashboard, Settings, Truck } from "lucide-react";
export function Shell({ children }: { children: React.ReactNode }) {
return (
<AppShell
name="Dispatch"
icon={<Truck />}
activePath={usePathname()}
renderLink={(props) => <Link {...props} />}
nav={[
{ label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
{ label: "Shipments", href: "/shipments", icon: <Truck className="size-5" /> },
]}
secondaryNav={[
{ label: "Settings", href: "/settings", icon: <Settings className="size-5" /> },
]}
>
{children}
</AppShell>
);
}There is no top bar. There used to be one, carrying a page title, the
launcher and the account menu - and it was a second, half-width strip of
chrome saying things the rail was better placed to say. The launcher and
the account row moved into the rail's footer, where they are one press from
wherever the pointer already is; the page title moved into the page, where
ConnectPageShell renders it next to that page's own actions and
breadcrumbs. What is left is a single column of chrome and a full-height
content pane, which is what every application this shell is modelled on
settled at.
The one exception is below md, where a slim bar carries the drawer
trigger. A phone has no rail to put it in, and a navigation you cannot open
is not a smaller navigation.
The rail starts collapsed. defaultCollapsed is true: the icon rail
is the resting state, and expanding is the deliberate act. That is the right
default because the rail's job is to be there, not to be read - a person
learns five glyphs in a day and then wants the 200px back for their data.
A stored preference from the user's last visit always wins over it.
There is no isSignedIn branch anywhere in here, and there should be none
in yours: the auth buttons render nothing once a session exists, the account
row renders nothing without one, and the launcher renders nothing when
signed out. The swap happens inside the components.
It renders no provider of its own - mount ConnectProvider above it, once,
so a page with a shell, a plan table and a usage meter still makes one
session read.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | AppShellProps |
Returns
Element
Function: buildCheckoutUrl()
function buildCheckoutUrl(__namedParameters: CheckoutUrlOptions): string;The Connect URL that completes a subscribe or a plan change.
Both land on a modal Connect renders over the relevant page (an intercepted
parallel route - app/@modal/(.)apps/... and app/@modal/(.)billing/...),
so the customer arrives already looking at the confirmation for the plan
they picked here, not at a catalogue they have to find it in again.
The subscribe path is /apps/..., not /applications/...: the portal
route customers see is /apps, and /applications is a Console-only
prefix that has never served this page. Building the other one is what
sent every first-time subscriber from a partner's pricing page to a 404.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | CheckoutUrlOptions |
Returns
string
Function: checkoutSubscriptionId()
function checkoutSubscriptionId(action: PlanAction, current:
| SubscriptionResponse
| null): string | null;The subscription id a checkout hand-off should carry for action - or
null, meaning "send them to Connect's subscribe screen".
Paired with resolveAction deliberately: the button's label and the URL it
opens are one decision, and deriving the URL from the mere presence of a
subscription row instead is what sent a card reading "Subscribe" to
Connect's change-plan screen, which answered notFound().
The gap is real rather than hypothetical. resolveAction needs the
current plan to classify a change and answers "subscribe" without
one - and SubscriptionResponse carries plan: null beside a perfectly
live subscription whenever that plan has been deleted from the catalogue
out from under it, which a re-seeded application does. Connect's change-plan
route is stuck in the same place for the same reason: it cannot prorate
against a price it cannot read.
Sending those to /apps/.../subscribe/... is a real hand-off, not a guess:
that route re-reads the workspace's live subscriptions server-side and
redirects to the change-plan screen itself when there genuinely is one to
change. Connect decides, so the grid does not have to be sure.
Parameters
| Parameter | Type |
|---|---|
action | PlanAction |
current | | SubscriptionResponse | null |
Returns
string | null
Function: ConnectAccessGate()
function ConnectAccessGate(__namedParameters: ConnectAccessGateProps): Element;Wrap your app's content once - typically in your root layout, alongside
(not instead of) your own header/nav - and it renders exactly one of: the
hard-block card for a suspended workspace, one with no plan, or one
waiting on approval; your children plus a dismissible payment-due banner
during an automatic dunning retry; or just children once everything is
clear.
Do not wrap your own plans/pricing page with this. The no_subscription
card's button sends someone to Connect's subscribe page directly (see
GateCard's doc comment on why - it used to link to your in-app /plans
instead, which is a dead end once this gate hides that very page too), but
a customer can still navigate to your plans page on their own (a nav link,
a bookmark) and must actually see it there. Since the gate is mounted once
around every route, carving out that exception is your router's job, not
a prop on this component: check the current path in your layout/shell and
skip wrapping children in <ConnectAccessGate> for that one route.
Signed-out renders children, not a fallback - deliberately different
from ConnectReadinessGate/ConnectSubscriptionGate, which show nothing
until signed in. None of the four states this component composes is a
property of a signed-out visitor, and a component mounted around your
entire app must not hide a public landing page behind an empty gate. Your
own sign-in flow decides what a signed-out visitor sees.
While the verdict is unknown, children render. The two reads behind
this gate are client-side round trips that happen on every page load, and
this component is mounted around an entire application - so blocking on
them replaced every partner's whole page with one placeholder until both
came back. Anything the host had streamed underneath was discarded: per-
section skeletons never painted, and a page built as a static shell with
suspended regions arrived all at once anyway. That is the common path, paid
by every workspace on every navigation.
Rendering children instead makes the loading case agree with the two
cases either side of it. A failed read already falls through to clear
("fail open rather than hard-block the whole app on a network hiccup", in
useConnectAccessGate), and a signed-out visitor already renders
children too - "we do not know yet" was the one unknown treated as a
refusal.
What it costs is honest and small: a genuinely blocked workspace sees its
app for the length of one round trip before the block card replaces it.
That is the rare path, it is self-correcting, and it is the same exposure
the failed-read branch has always accepted. Pass loading to opt back into
a placeholder for a gate around one small region.
This is a UX gate, not a security boundary - same caveat as every
other gate in this package, and the reason the trade above is available at
all. The authoritative check is client.entitlements.getReadiness on your
server, before you do the thing.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAccessGateProps |
Returns
Element
Function: ConnectAccountSessions()
function ConnectAccountSessions(__namedParameters: ConnectAccountSessionsProps): Element | null;Every device signed into this Connect account - not scoped to this
application, see ConnectUserSession's doc comment. Owns the one
useConnectSessions() call for this modal; every revoke action here
mutates through it and re-reads the list itself, since nothing else
invalidates it automatically - see mutateResource's doc comment.
That re-read is invalidateResource("/sessions") then refetch(),
and both halves are load-bearing. mutateResource evicts only the path it
posted to - /sessions/revoke - so /sessions keeps its cached promise,
and refetch() alone just re-runs the effect and is handed that same
resolved promise back. The row stayed on screen and the revoke looked like
it had silently failed, when in fact the server had done exactly as asked.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAccountSessionsProps |
Returns
Element | null
Function: ConnectAppsGrid()
function ConnectAppsGrid(__namedParameters: ConnectAppsGridProps): Element | null;The Google-style app launcher: a grid icon that opens every Connect application this workspace has, and every one it could have.
Two sections, because they are two different actions. Your apps is what the workspace can open right now - the current application pinned first and marked, so the grid never has a hole where "you are here" should be, and a paused subscription still listed but badged, because a dunning cycle should not make a product look deleted. Explore is the rest of the active catalog, minus anything this workspace's type could never subscribe to, and a tile there opens that application's page on Connect.
It used to show subscriptions only, with a "Browse all applications" link under the grid pointing at a Connect route that does not exist - so a workspace with one subscription saw one tile and had no working way to find anything else. Discovery belongs in the grid.
Renders nothing when signed out: a launcher with nothing to launch is just a button that opens an empty box.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAppsGridProps |
Returns
Element | null
Function: ConnectAskAi()
function ConnectAskAi(__namedParameters: ConnectAskAiProps): Element | null;Ask AI - a chat panel grounded in the signed-in workspace's Context Index, streamed through your own bridge.
Answers are not only prose. Connect's model can call a small set of
presentation tools - showTable, showChart, showMetrics, showTimeline,
showDetails, showSteps, showSources - and this panel renders each one
natively: a sortable table with a CSV download, a chart, a row of metric
tiles. Values arrive raw with a format tag, so they are formatted here rather
than by the model, which is what lets a column sort numerically.
Nothing here holds a credential: it POSTs to ${basePath}/ai/chat on your
own origin, and createConnectHandler is what reaches Connect. The workspace
is resolved from the caller's session there, never sent from the browser - so
a user can only ever ask about the workspace they are signed into.
<AppShell name="Dispatch" nav={nav}>
{children}
</AppShell>AppShell mounts it by default. Mount it directly only when you are not
using the shell: it renders a fixed corner trigger plus a panel that is an
in-flow flex sibling at lg and up, so opening it pushes your content
aside rather than covering it. That requires it to be the last child of a
flex row owning the viewport height - which is exactly what AppShell is.
Below lg it is a full-screen sheet with a backdrop instead; there is no
room to push anything on a phone.
Host setup: markdown rendering needs streamdown installed, one Tailwind
@source "…/node_modules/streamdown/dist/*.js" line so its utility classes
are generated, and import "streamdown/styles.css" for the streaming
animation. The colour tokens Streamdown expects are already mapped in this
SDK's theme.css.
Answers require the workspace's Context Index to be turned on (a workspace
admin toggle in Connect) and your API key to carry the ai:chat scope.
Neither is something the browser can discover, so both surface as a message
in the panel rather than as a hidden button.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAskAiProps |
Returns
Element | null
Function: ConnectAvailabilityFormModal()
function ConnectAvailabilityFormModal(__namedParameters: Readonly<ConnectAvailabilityFormModalProps>): Element;Creates or edits a workspace's working hours, from inside a host application rather than a link-out to Connect.
Deliberately narrower than Connect's own Calendar settings form: one interval per day (no split shifts), no date overrides, and the timezone is set once at creation (from the browser's own) and shown but not editable here afterwards. This is a first-party setup-and-edit surface, not a second copy of the Console editor - "Manage in Connect" is still where the rest of what a schedule can do lives.
Not optimistic. The write settles against the server before this
closes, the same posture every write in this package takes - see
ConnectBookingDialog.
This outer component only decides whether the row to edit has loaded yet;
the form itself is ScheduleFields below, mounted with
key={editing?.id} - see that component's own doc comment for why.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectAvailabilityFormModalProps> |
Returns
Element
Function: ConnectBookingCalendar()
function ConnectBookingCalendar(__namedParameters: Readonly<ConnectBookingCalendarProps>): Element;A week of the workspace's calendar, with booking, rescheduling and cancelling.
Two views, and which one you get is a viewport decision, not a
preference. A seven-column time grid needs roughly 640px before a column
is wide enough to hold a name; below that it degrades into unreadable
slivers. So the grid is sm: and up, and the same week renders as a grouped
agenda underneath - not a cut-down grid, a different and better shape for a
phone. Both read the same data and open the same dialogs.
Everything it does goes through the bridge as the signed-in member, so
Connect checks their calendar:read / calendar:manage rather than the
host application's key. A member who may look but not book gets the grid and
a refusal on the write, which is the correct shape - see useBookingActions.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectBookingCalendarProps> |
Returns
Element
Function: ConnectBookingDetail()
function ConnectBookingDetail(__namedParameters: Readonly<{
booking: Booking;
eventType: | BookableEventType
| null;
timeZone: string;
onOpenChange: void;
onChanged: void;
}>): Element;One appointment, and the two things anyone opens it to do.
Rescheduling re-reads the slots with excludeBookingId set to this booking.
Without that the appointment blocks its own replacement, and moving it by
half an hour - the only move most people ever make - offers nothing.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ booking: Booking; eventType: | BookableEventType | null; timeZone: string; onOpenChange: void; onChanged: void; }> |
Returns
Element
Function: ConnectBookingDialog()
function ConnectBookingDialog(__namedParameters: Readonly<{
isOpen: boolean;
onOpenChange: void;
eventTypes: BookableEventType[];
initialStartAt: Date;
timeZone: string;
onBooked: void;
}>): Element;Picking a time, and then who it is for.
The slot list is read fresh every time this opens and is never cached across an open: it is a snapshot of contention, and offering a time somebody took ninety seconds ago is how two people end up in the same slot. The server re-checks inside its transaction regardless, so a stale list costs a refusal rather than a double booking - this only makes the refusal rare enough to be surprising when it happens.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ isOpen: boolean; onOpenChange: void; eventTypes: BookableEventType[]; initialStartAt: Date; timeZone: string; onBooked: void; }> |
Returns
Element
Function: ConnectCallButton()
function ConnectCallButton(__namedParameters: ConnectCallButtonProps): Element | null;Rings phone from the workspace's number. Needs <ConnectCallProvider>
above it, and a member with "Make calls" - render it only for them, since a
button the server refuses for everyone else helps nobody.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectCallButtonProps |
Returns
Element | null
Function: ConnectCallingStatus()
function ConnectCallingStatus(__namedParameters: ConnectCallingStatusProps): Element;The workspace's calling at a glance - its business number, the minutes left, and the next step - for a settings page. Reads the same availability the Call buttons do, so the two never disagree. Buying minutes and adding a number happen in Connect, and come back here when done.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectCallingStatusProps |
Returns
Element
Function: ConnectCallProvider()
function ConnectCallProvider(__namedParameters: ConnectCallProviderProps): Element;Holds the call in progress and draws the in-call bar. See the module comment.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectCallProviderProps |
Returns
Element
Function: ConnectConfirmDialog()
function ConnectConfirmDialog(__namedParameters: Readonly<ConnectConfirmDialogProps>): Element;The confirmation in front of a destructive action.
It closes on confirm, not on response. Holding the modal open until the server answers makes every delete feel like a form submission, and the error path already has somewhere to go - a toast, or the row reappearing. A dialog that stays open to report failure is right only where the failure is expected and correctable in place (a seat cap, a last-admin invariant), and that is a blocking form, not this.
onConfirm may return a promise. It is not awaited before closing, but a
rejection is caught here rather than escaping - an error thrown out of an
async transition reaches the nearest error boundary and takes the page down.
The anatomy is HeroUI's: the Modal root owns the open state,
Modal.Container the sizing, Modal.Dialog the focus trap. The trigger
is a direct child of the root, never wrapped in Modal.Trigger.
That wrapping is what this component used to do, and it meant the dialog
never opened for any caller passing a <Button> - which was all of them.
Modal.Trigger renders a div role="button" inside React Aria's
Pressable and relies on a plain onClick bubbling up to it; a React Aria
button nested inside calls e.stopPropagation() in its own usePress
click handler, so the click never arrives. Nothing failed loudly - the
press was simply eaten. The root is DialogTrigger, whose PressResponder
the trigger registers with directly, so there is no nesting and no
propagation to lose.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectConfirmDialogProps> |
Returns
Element
Function: ConnectCreateWorkspaceModal()
function ConnectCreateWorkspaceModal(__namedParameters: Readonly<ConnectCreateWorkspaceModalProps>): Element;Opens another workspace under the signed-in user's own tenant - a branch office, a second business unit - from inside a host application rather than a link-out to Connect.
Only the tenant's owner can use this successfully. CreateWorkspaceUseCase
refuses anyone who merely belongs to a colleague's tenant, with a message
naming that - someone who wants a workspace of their own uses a personal
account instead.
The tenant's plan decides how many workspaces it may hold, and this does not pre-check it. There is no capacity read to show ahead of time - the same posture Connect's own "Create workspace" button takes - so a refusal surfaces here exactly as the server wrote it ("Your current plan covers a single workspace - subscribe to a plan that includes additional workspaces to open another.", or "You've used all 3 of the workspaces your plans include. Upgrade a plan or delete a workspace to open another.").
Does not switch the session to the new workspace. Creating one never
does, on Connect's own screen either - see useCreateWorkspace's doc
comment for how to chain a switch once you have the result, and guard that
chain on inAudience the same way <ConnectWorkspaceSwitcher> already
does for every other row.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectCreateWorkspaceModalProps> |
Returns
Element
Function: ConnectCursorPagination()
function ConnectCursorPagination(__namedParameters: Readonly<ConnectCursorPaginationProps>): Element;Next / previous for a cursor-paginated list.
Cursor rather than page numbers, and the component shape follows from that: there is no "page 7 of 42" because a keyset-paginated list does not know how many pages there are without a second count query over the same filter - which is a billed index scan for a number nobody acts on. What a user does act on is "more" and "back", and those are exact.
The count is announced in a live region so a screen-reader user learns that filtering or paging changed the result set. Without it the table silently swaps its contents.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectCursorPaginationProps> |
Returns
Element
Function: ConnectEventTypeFormModal()
function ConnectEventTypeFormModal(__namedParameters: Readonly<ConnectEventTypeFormModalProps>): Element;Creates or edits an appointment type, from inside a host application rather than a link-out to Connect.
Needs a schedule to exist first - the picker reads useAvailabilitySchedules
and renders "Set up availability first" with nothing to submit if none
exists yet. Pair this with ConnectAvailabilityFormModal in a setup flow
rather than assuming one.
That gate waits for the read, and the order matters. Both bridge reads
answer [] while they are in flight, so testing schedules.length === 0
first showed "Set up availability first" to everybody who opened this -
including, for a second or two, somebody pressing Edit on an appointment
type that provably has a schedule. A load state wearing an empty state's
words is worse than a spinner: it reads as a verdict, and on a slow
connection it is the only thing the modal ever says. A failed read is named
as a failure for the same reason - it is not the same fact as "none exist".
Host is a specific member, never a team. There is no groups read in this bridge, so a round-robin team host stays a Console-only capability - defaults to whoever is signed in on create, with an optional picker from the workspace's own roster. Editing a group-hosted row is refused inline - the modal shows a notice instead of a form, because this surface cannot represent (and so cannot safely resend) a team host.
This outer component only decides which message or form to show; the
form itself is EventTypeFields below, mounted with key={editing?.id} -
see that component's own doc comment for why.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectEventTypeFormModalProps> |
Returns
Element
Function: ConnectFeatureGate()
function ConnectFeatureGate(__namedParameters: ConnectFeatureGateProps): Element;Renders children only when the workspace's plan includes feature.
<ConnectFeatureGate feature="advanced_reports" title="Reports are on Growth">
<Reports />
</ConnectFeatureGate>Like every gate in this package, it is a UX gate, not a security
boundary: the entitlement is read through your own origin and the browser
can ignore it. Check client.entitlements.hasFeature(workspaceId, feature) on the
server before doing the thing the feature unlocks.
Renders loading - not fallback - while the answer is unknown, so a paying
customer never sees an upsell for something they already have.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectFeatureGateProps |
Returns
Element
Function: ConnectFilterTabs()
function ConnectFilterTabs<TValue>(__namedParameters: Readonly<ConnectFilterTabsProps<TValue>>): Element;A single-select filter strip - the "All / Open / Won / Lost" row above a list.
Real radio semantics rather than a row of buttons: it is a single choice
from a closed set, so arrow keys should move between options and the
selected one should be announced as selected. A group of <button>s gets
neither, and that is the usual hand-rolled shape.
Wraps rather than scrolling on a narrow viewport. See the note on the container below: a horizontal scroller nested in a vertically scrolling page hides its overflow rather than deferring it.
Type Parameters
| Type Parameter |
|---|
TValue extends string |
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectFilterTabsProps<TValue>> |
Returns
Element
Function: ConnectGrievanceNotice()
function ConnectGrievanceNotice(__namedParameters: Readonly<ConnectGrievanceNoticeProps>): Element | null;The DPDP s.13 grievance channel, rendered.
Every application on the platform is a Data Processor under one notice, so the wording lives here rather than being retyped per application - a user comparing two applications' privacy pages should not find two different accounts of who their Data Fiduciary is.
Renders nothing at all while loading and nothing on failure, rather than a
skeleton or an error. That is unusual here and deliberate: this component
is almost always inside a larger privacy page that has its own content, and
a red error box in the middle of a legal notice reads as the notice itself
being broken. The address is also on the public web either way. Pass
useConnectCompanyProfile() directly to your own layout if you want to
handle those states yourself.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectGrievanceNoticeProps> |
Returns
Element | null
Function: ConnectHomeButton()
function ConnectHomeButton(__namedParameters: Readonly<{
className?: string;
}>): Element;A plain "Open Connect" button, for a footer or a help menu.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ className?: string; }> |
Returns
Element
Function: ConnectIntegrationsModal()
function ConnectIntegrationsModal(__namedParameters: ConnectIntegrationsModalProps): Element | null;A grid of every Composio-hosted toolkit this workspace can connect -
Google Calendar, Slack, Twitter/X, whatever a platform admin enabled in
Connect's own Console. Each tile carries its own connect/reconnect/
disconnect action and its own pending state, independent of every other
tile - see the doc comment on pendingSlug below for why that matters.
Not optimistic: a tile stays whatever it already was after "Connect" is
pressed, because the connection isn't settled until Composio's webhook
confirms it - mutateResource here only ever gets as far as pending
before the browser navigates away to complete the provider's OAuth screen.
Renders nothing when signed out, same as ConnectAppsGrid.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectIntegrationsModalProps |
Returns
Element | null
Function: ConnectInvoicesButton()
function ConnectInvoicesButton(__namedParameters: Readonly<{
className?: string;
}>): Element | null;Invoices and payment history, on Connect.
A deep link rather than a component that lists them, and that is not a
placeholder: nothing in /api/v1/application/* exposes invoices, so an SDK-side
list would have nothing to read. It is also the right answer regardless -
a GST tax invoice is a legal document with a fixed format, downloadable and
emailed from one place, and a partner-rendered copy of one would be a second
version of a record that must have exactly one.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ className?: string; }> |
Returns
Element | null
Function: ConnectLinkButton()
function ConnectLinkButton(__namedParameters: Readonly<ConnectLinkButtonProps>): Element;A HeroUI Button that renders a link.
HeroUI v3's Button has no as prop, so <Button as={Link}> does not type
check and does not work - this is the supported route, via render.
The cast is the same one HeroUI's own "Render Function" example needs:
render types its argument as the props of the element Button would have
emitted, which is a <button>, including a ref typed to
HTMLButtonElement. Spreading that onto an anchor is sound at runtime and
unrepresentable in the type, which is what the cast is for.
Why passing every handler through is safe for navigation (React Aria
3.51 usePress): nothing on the press path calls preventDefault on an
anchor's click except when the button is disabled - which is the one
case it should stop. A mouse click is turned into onPress and allowed to
proceed; Enter is left to the browser (shouldPreventDefaultKeyboard is
false for an <a href>), whose native click then does the same; Space is
not treated as a press on a link at all, matching link semantics. next/link
calls the passed onClick first and navigates unless defaultPrevented, so
it still navigates. The click handler does stopPropagation, as every React
Aria button does - ancestors' onClick do not see it.
React Aria still logs one development-only warning - "Expected <button>, got <a>" - because the ref now reaches a real element and it can see the tag. That is the honest description of this component, and the alternative (dropping the ref) is what produced the two warnings this replaced.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectLinkButtonProps> |
Returns
Element
Function: ConnectLoginButton()
function ConnectLoginButton(__namedParameters: ConnectAuthButtonProps): Element | null;"Continue with Connect" - the federated sign-in button, in the shape people already know from Google and GitHub: neutral surface, brand mark on the left, one line of text.
variant="secondary" by default rather than primary, on purpose. A
federated button that outshouts the host app's own primary action reads as
an ad; every mature identity button is quiet. Pass variant to override if
Connect is the primary path in your flow.
Hidden while a session is still loading, so a signed-in user never sees a Login button flash before their avatar resolves.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAuthButtonProps |
Returns
Element | null
Function: ConnectLogo()
function ConnectLogo(__namedParameters: ConnectLogoProps): Element;The shared platform lockup: an application's own glyph on the CosmoOps badge,
cosmo·ops beside it, and the application name underneath.
This is what makes a family of application platforms look like one product. Each
platform passes only what differs - its name and its icon - and inherits
the mark geometry, the wordmark treatment, the separator, and the accent from
one place. Change the lockup here and every platform follows.
Sizes are em-based throughout, so the whole lockup scales from the
wrapper's font size: className="text-base" renders it half again as large
as text-sm, with the badge, gap and tracking all in proportion. That is
why there is no size prop.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectLogoProps |
Returns
Element
Function: ConnectManageSubscriptionButton()
function ConnectManageSubscriptionButton(__namedParameters: Readonly<{
className?: string;
}>): Element;The "Manage subscription" action on its own, for layouts that want it visible rather than buried in the account menu - a billing banner, an empty state, a settings page.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ className?: string; }> |
Returns
Element
Function: ConnectMark()
function ConnectMark(__namedParameters: Readonly<{
className?: string;
}>): Element;The Connect mark - concentric rings on a solid accent badge.
Painted from --accent / --accent-foreground, the same HeroUI tokens the
host app themes, so the badge tracks whatever palette an application platform has
configured rather than carrying a hardcoded CosmoOps green into someone
else's brand.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ className?: string; }> |
Returns
Element
Function: ConnectNoSubscriptionCard()
function ConnectNoSubscriptionCard(__namedParameters: ConnectNoSubscriptionCardProps): Element;The "you are not on a plan yet" card.
Points at your own /plans page rather than opening Connect, and that is
the deliberate part: choosing a plan is browsing, it belongs in your product
where your pricing story is, and only the final confirmation needs to happen
on Connect. Sending someone straight out of the app to pick is a context
switch two steps too early.
Rendered on its own it is just a card; ConnectSubscriptionGate is the usual
way in, since it picks this only when there is genuinely nothing to show.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectNoSubscriptionCardProps |
Returns
Element
Function: ConnectOneTap()
function ConnectOneTap(__namedParameters: ConnectOneTapProps): Element | null;Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectOneTapProps |
Returns
Element | null
Function: ConnectPlans()
function ConnectPlans(__namedParameters: ConnectPlansProps): Element;The plan table: monthly/annual toggle, features, add-ons, the discount actually in effect, and the subscribe / upgrade / downgrade flows.
Where the flow runs, and where it finishes
The flow is here: picking a plan, seeing what it costs with GST, being told what happens to the cycle you are part-way through, and confirming. The payment is not, and that is deliberate. A plan change is not one write - it re-runs the readiness gates, prices proration against accumulated credit, re-checks the RBI mandate ceiling for the new amount, and issues a GST invoice. Connect owns that sequence end to end; a partner-side "change plan" call would be a second implementation of it that drifts. It would also mean a browser holding the partner's credentials could move money, which is the one thing the bridge exists to prevent.
So confirming opens Connect - on the exact confirmation screen for the
plan just picked, via an intercepted parallel route, not a catalogue the
customer has to search again - and this component watches /entitlements
until the change lands, then repaints itself. See useConnectCheckout.
Coupon codes work the same way: the field carries the code into Connect's checkout, where it is validated and applied against the real cart. Anything this component claimed about a code would be a guess it cannot honour.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectPlansProps |
Returns
Element
Function: ConnectPlansPage()
function ConnectPlansPage(__namedParameters: ConnectPlansPageProps): Element;A complete /plans page, assembled from the pieces so the host app's route
file is one line:
// app/plans/page.tsx
"use client";
export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";The assembly is the value here, not the layout. Three things have to be on this page and are easy to leave off:
- The readiness banner, above the grid. A workspace that still needs KYC can press Subscribe all day and be refused at the last step. Saying so before they pick is the difference between a page that works and one that ends in a rejected charge.
- What they're on now, so "Upgrade" on a card has something to mean.
- A signed-out state that is a login button, not an empty grid - the catalogue reads through the bridge, which needs a session.
Every prop ConnectPlans takes passes straight through, and each piece is
exported on its own for an app that wants a different arrangement.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectPlansPageProps |
Returns
Element
Function: ConnectPlatformGate()
function ConnectPlatformGate(__namedParameters: ConnectPlatformGateProps): Element;Renders children only for a Connect platform staff session.
<ConnectPlatformGate>
<LinkMasterConsole />
</ConnectPlatformGate>Staff are Connect's own operators - platform_admin, platform_manager,
developer - signing into your application. They belong to no workspace,
so a staff session carries no workspaceId, no subscription and no member
role; see SessionAccess.staff. Staff access is off by default and turned on
per application by a Connect platform admin, so this gate renders nothing
until that happens.
This is a UX gate, not a security boundary - the same caveat every gate
in this package carries, and it matters more here than anywhere else,
because what is behind this one is usually an operator console. The browser
can ignore this component entirely. Every route it reveals must re-check
staff on your own server:
const { staff, role } = await client.identity.getUser(sessionToken);
if (!staff) return forbidden();Renders loading - not fallback - while the session is unknown, so an
operator never flickers through the customer layout on the way in.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectPlatformGateProps |
Returns
Element
Function: connectPortalUrl()
function connectPortalUrl(
appUrl: string,
clientId: string,
path: string): string;Any Connect portal page, with the context it needs to offer a way back
here - connectPortalUrl(appUrl, clientId, "/profile"),
connectPortalUrl(appUrl, clientId, "/calendar/settings"), and so on.
Connect answers any of these paths for a signed-in user on the bare URL -
these two params only add the "Back to <your app>" banner
(the hand-off strip in ShellAnnouncements on Connect's side). It validates return_to
against this client_id's registered redirect origins and silently drops
the banner if they don't match, so a wrong or stale appUrl/clientId
pairing degrades to what linking there did before rather than erroring.
window.location.href rather than a configured home URL: the point is to
come back to the page they left, and only the browser knows which one that
is.
Parameters
| Parameter | Type |
|---|---|
appUrl | string |
clientId | string |
path | string |
Returns
string
Function: ConnectProfileButton()
function ConnectProfileButton(__namedParameters: Readonly<{
className?: string;
}>): Element | null;The "Profile" action on its own - the third of the three Connect
destinations, alongside ConnectManageSubscriptionButton (Billing) and
ConnectHomeButton.
It was reachable only from inside ConnectUserMenu, which meant an app that
builds its own account menu had to hardcode ${appUrl}/profile to offer it -
and got no return_to, so the user had no way back.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ className?: string; }> |
Returns
Element | null
Function: ConnectProfileModal()
function ConnectProfileModal(__namedParameters: ConnectProfileModalProps): Element | null;The account modal with no trigger of its own, driven by the host.
For an application that already has an account control it wants to keep -
an avatar in its own chrome, a "Manage account" row in its own settings
page - and wants Connect's modal behind it rather than rebuilding identity,
sessions and sign-out. ConnectUserButton is the same modal with the
trigger supplied; reach for this one only when that trigger is in the way.
Renders nothing when signed out, open or not - there is no account to show.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectProfileModalProps |
Returns
Element | null
Function: ConnectProvider()
function ConnectProvider(__namedParameters: ConnectProviderProps): Element;Wraps the part of your app that shows Connect UI.
Holds one session read, shared by every component below it, so a page with a
user menu, an apps grid and a plan table makes one request rather than three.
There is no API key here and there cannot be: everything goes to your own
origin, where createConnectHandler holds the credential.
<ConnectProvider>
<App />
</ConnectProvider>Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectProviderProps |
Returns
Element
Function: ConnectReadinessBanner()
function ConnectReadinessBanner(__namedParameters: ConnectReadinessBannerProps): Element | null;Every gate between this workspace and your application, with the way out of each.
This is the component that keeps a blocked customer from staring at a broken
app. getReadiness resolves subscription state, KYC/KYB, a chargeable
payment method and seat headroom in one call, and each blocker carries a
resolveUrl into the exact Connect screen that fixes it - so being blocked
comes with somewhere to go rather than a refusal.
Render it high on any page your application gates. It draws nothing at all when the workspace is ready, so it is safe to leave mounted permanently.
Two deliberate refusals:
- Unrecognised codes are still shown, and still treated as blocking.
Connect adds codes without a major version (see
ReadinessBlockerCode), so a switch that only knew today's codes would silently drop tomorrow's - the one failure mode where a customer is locked out with no explanation. messageis rendered, never parsed. It is human-facing and subject to rewording; branching on its text would break on a copy edit.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectReadinessBannerProps |
Returns
Element | null
Function: ConnectReadinessGate()
function ConnectReadinessGate(__namedParameters: ConnectReadinessGateProps): Element;Renders children only once the workspace clears every blocking gate.
The hard version of ConnectReadinessBanner, for a route that genuinely
cannot function while blocked. Prefer the banner where the page still does
something useful - a gate that hides a whole screen behind a verification
notice is a worse experience than one that lets people look around while
they sort it out.
This is a UX gate, not a security boundary. The readiness read reaches
your own origin and a determined browser can ignore whatever it says. The
authoritative check is client.entitlements.getReadiness on your server,
before you do the thing.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectReadinessGateProps |
Returns
Element
Function: ConnectRegisterButton()
function ConnectRegisterButton(__namedParameters: ConnectAuthButtonProps): Element | null;The same button pointed at registration (prompt=create).
Only the starting screen differs - each screen links to the other carrying the same in-flight request, and anyone who already has a Connect session skips straight to consent either way. So shipping both buttons costs nothing and matches what users expect from a "Sign up" / "Log in" pair.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectAuthButtonProps |
Returns
Element | null
Function: ConnectSeatUsage()
function ConnectSeatUsage(__namedParameters: Readonly<{
plan: SubscribedPlan;
detail: SubscriptionDetail;
className?: string;
}>): Element | null;Seats used against how many may be used - what a "fixed" plan includes, or
the seats a "per-seat" subscription has bought (detail.seatCapacity).
Only meaningful for "fixed" and "per-seat" - "unlimited" has nothing to
count against, so it renders nothing rather than a meter that is always
empty. On "per-seat" a full count says what one more costs, because the
next assignment is refused until a seat is bought.
detail.seatCount is a live count of the members holding a role for this
application. It used to be the subscription's last-billed snapshot, which
only ever moved on a "per-seat" plan - so a "fixed" single-seat plan
read "0 of 1 seat used" permanently, including when its one seat was taken
and Connect was already refusing the next assignment.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ plan: SubscribedPlan; detail: SubscriptionDetail; className?: string; }> |
Returns
Element | null
Function: ConnectShellProvider()
function ConnectShellProvider(__namedParameters: Readonly<{
value: ConnectShellContextValue;
children: ReactNode;
}>): Element;Shared state for every part of the shell, so ConnectSidebarNav,
ConnectWorkspaceSwitcher and ConnectUserButton all know whether the rail
is collapsed without the host threading a prop through three layers.
This is the piece that makes the shell composable rather than only configurable: the parts are exported and can be rearranged, and they still agree about collapse, routing and the mobile drawer because they read one context instead of one another's props.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ value: ConnectShellContextValue; children: ReactNode; }> |
Returns
Element
Function: ConnectSidebar()
function ConnectSidebar(__namedParameters: ConnectSidebarProps): Element;The application rail: a fixed-width column that animates between an icon-only 64px and a labelled 264px.
Why width, and nothing else, animates. Every row inside puts its glyph
on the same 32px centre in both states (see RAIL_COLLAPSED), and every
label is clipped rather than re-measured (see SIDEBAR_ROW). So the only
property in flight is this element's width: no child moves, no text
re-wraps, and there is no per-frame layout pass on the rows. That is why the
collapse looks smooth rather than merely fast.
Hidden below md. The mobile drawer renders the same children with
collapsed: false, because a 64px icon rail on a phone is a worse drawer,
not a smaller one.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSidebarProps |
Returns
Element
Function: ConnectSidebarBody()
function ConnectSidebarBody(__namedParameters: Readonly<{
children: ReactNode;
}>): Element;The rail's scrolling region, for the primary navigation. Everything else in
the sidebar is shrink-0, so this is the only part that ever gives way.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ children: ReactNode; }> |
Returns
Element
Function: ConnectSidebarFooter()
function ConnectSidebarFooter(__namedParameters: Readonly<{
children: ReactNode;
separated?: boolean;
}>): Element;A pinned region at the foot of the rail - the secondary nav, the launcher, the account row.
separated draws the hairline above it. The account row wants one; a
secondary nav sitting directly on top of the account row does not, or the
rail ends in two rules a few pixels apart.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ children: ReactNode; separated?: boolean; }> |
Returns
Element
Function: ConnectSidebarHeader()
function ConnectSidebarHeader(__namedParameters: ConnectSidebarHeaderProps): Element;The brand row, and the rail's own expand/collapse control.
Two states, one DOM. The badge is mounted once and never re-created; the wordmark fades beside it and the collapse button fades at the trailing edge. Swapping which element renders the brand would remount it halfway through the width animation and flicker.
Collapsed, hovering or focusing the badge swaps it for the expand glyph, so
the rail needs no permanent control eating a row. Expanded, the control sits
at the header's trailing edge, where it can be aimed at without covering the
brand. The swap is pure CSS on a group - hovering re-renders nothing,
which is what keeps it instant.
The trade this makes, deliberately: collapsed, the lockup is no longer a link home. It has become the expand control, and one target that navigates on click but expands on click-after-hover would be worse than either. Put home in the nav; every application already does.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSidebarHeaderProps |
Returns
Element
Function: ConnectSidebarNav()
function ConnectSidebarNav(__namedParameters: ConnectSidebarNavProps): Element;The sidebar's nav column on its own - sections, items, one level of children, and the active highlight.
AppShell renders this inside its rail and its mobile drawer, and it
is exported for a host that wants the shell's navigation without its chrome:
the same items, the same active resolution, dropped into a layout you
already have.
Every prop is optional except nav, because inside a shell they are all
inherited from it. Passing one wins over what the shell says - which is how
a second nav column (see secondaryNav) can share the rail's collapse state
while keeping its own items.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSidebarNavProps |
Returns
Element
Function: ConnectSidebarRow()
function ConnectSidebarRow(__namedParameters: ConnectSidebarRowProps): Element;A pressable row in the rail that is not a nav item - the launcher, the account button, anything a host adds to the footer.
Exported so a host's own row lines up with the SDK's. It applies the same 32px glyph centre and the same label fade, which is what stops a partner's "Help" row drifting sideways while every row above it holds still.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSidebarRowProps |
Returns
Element
Function: ConnectSignOutButton()
function ConnectSignOutButton(__namedParameters: Readonly<{
onSignOut?: () => void;
className?: string;
}>): Element | null;Sign out on its own, for a settings page that wants it as a destructive row.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ onSignOut?: () => void; className?: string; }> |
Returns
Element | null
Function: ConnectSubscribeDialog()
function ConnectSubscribeDialog(__namedParameters: Readonly<ConnectSubscribeDialogProps>): Element;The review step before a customer is sent to Connect.
It exists because the hand-off is a context switch, and a context switch the customer did not expect reads as being dumped somewhere. So this says what they are about to buy, what it costs with GST, what happens to the cycle they are part-way through, and - plainly - that the payment itself happens on Connect. Then one button, which opens it.
Every number here is a preview, and deliberately never the last word. The authoritative price is computed by Connect against the real cart: proration is priced against the actual cycle, a coupon is validated against the plan it is being applied to, trial eligibility depends on whether this workspace has ever subscribed to this application before, and an accumulated credit is deducted. None of that is knowable from the partner's origin, so this component states the list price and says where the final figure comes from rather than quietly guessing at one.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSubscribeDialogProps> |
Returns
Element
Function: ConnectSubscriptionCard()
function ConnectSubscriptionCard(__namedParameters: ConnectSubscriptionCardProps): Element;The current subscription at a glance: plan, what it costs, what happens next, and one way into Connect to change it.
The counterpart to ConnectNoSubscriptionCard - the same slot in the
signed-up state - so an account or settings page can render this and get the
right thing either way.
Dates are formatted, never computed. "Renews on 3 Mar" is a fact from the wire; "renews in 12 days" would be a clock read during render, which is impure and re-renders wrong.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSubscriptionCardProps |
Returns
Element
Function: ConnectSubscriptionGate()
function ConnectSubscriptionGate(__namedParameters: ConnectSubscriptionGateProps): Element;Renders children for a subscribed workspace, and the "pick a plan" card for
everyone else.
Never renders the fallback while loading. That flash - "you're not subscribed" for 200ms, then the real content - is the specific bug this component exists to make impossible, and it is what hand-rolled versions of it get wrong: the honest states are "still asking" and "asked, and the answer is no", and only the second one is a sales pitch.
This is a UX gate, not a security boundary - same caveat as
ConnectReadinessGate. Anything that must not happen without a subscription
is checked on your server with client.entitlements.hasFeature.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectSubscriptionGateProps |
Returns
Element
Function: ConnectTrialBanner()
function ConnectTrialBanner(__namedParameters: ConnectTrialBannerProps): Element | null;A trial that is running, and when it converts.
Renders only while trialing, so it can live permanently in a layout. It says
the charge date rather than a countdown for the same reason
ConnectSubscriptionCard does - a countdown means reading the clock while
rendering.
The wire carries isTrialing but no trial end date; nextBillingDate on a
trialing subscription is the first charge, which is the date a customer
actually cares about anyway.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectTrialBannerProps |
Returns
Element | null
Function: ConnectUsageMeter()
function ConnectUsageMeter(__namedParameters: ConnectUsageMeterProps): Element | null;How much of each consumable add-on this workspace has left.
Reads the balance Connect actually debits, so the number here and the number
consumeAddon enforces cannot disagree. Before the addons:read endpoint
existed this component was impossible to write honestly: an application could
spend an allowance down on every request and the only thing the API exposed
was the list of add-on ids on the subscription.
It shows balance against includedPerCycle, and never a "used" figure.
With carryForward an unspent balance survives the cycle boundary, so
included - balance can go negative - a meter that runs backwards. Real
consumption for a cycle is a ledger question, and computing it from a
balance would be a guess rendered as a fact.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectUsageMeterProps |
Returns
Element | null
Function: ConnectUserButton()
function ConnectUserButton(__namedParameters: ConnectUserButtonProps): Element | null;The signed-in account, as a control that opens the account modal.
In "row" form this is the last thing in the sidebar, which is where an
account control belongs: it is the one item in the rail that is about you
rather than about the product, and the foot of a column is where every
application a customer already uses puts that. It collapses with the rail -
the avatar holds its position and the name fades - because the avatar alone
is still a complete account control, which is exactly what an icon rail
needs.
Renders nothing when signed out. Pair it with <ConnectLoginButton>, which
renders nothing when signed in, and the two swap with no branch in your
own layout.
On the HeroUI shape: the trigger is a plain <Button> as a direct child
of <Modal>, not wrapped in <Modal.Trigger>. Dropdown.Trigger - and by
the same construction Modal.Trigger - is itself a React Aria Button, so
wrapping one produces a button inside a button: invalid markup, and press
handling stops working.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectUserButtonProps |
Returns
Element | null
Function: ConnectUserMenu()
function ConnectUserMenu(__namedParameters: ConnectUserMenuProps): Element;Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectUserMenuProps |
Returns
Element
Deprecated
Renamed to ConnectUserButton in 0.12 - it is one button that
opens a modal, and calling it a "menu" had people looking for a dropdown
that never existed. compact maps to variant="compact", and the old
default (avatar plus name) to variant="inline". This alias will be removed
in a later minor.
Function: ConnectVisitorChat()
function ConnectVisitorChat(__namedParameters: ConnectVisitorChatProps): Element | null;The public chat bubble: a visitor with no account asks about your product, books a call, or gets started.
It runs on pages that have no ConnectProvider
Deliberately, and it is the main thing separating this component from every
other one here. A marketing page usually has no session to hold and nothing
on it that needs one, so requiring the provider would mean wrapping a
landing page in an auth context to put a chat bubble on it. It reads the
context through useConnectOptional and uses it only for basePath and the
injected fetch; both have props, and both have defaults.
Nothing happens until somebody opens it
The conversation is minted on first open, never on mount. On a busy marketing site mounting would mean one Firestore write and one wasted session row per page view, for a bubble most visitors never touch - and the bubble itself needs no server state to render. The greeting arrives with that first call and is stored copy rather than generated, so opening costs one fast round trip and the visitor is reading real words immediately.
It is built for a phone first
Open, it is a full-height sheet on a narrow viewport and an anchored panel
from sm up - a marketing chat that covers a desktop browser is harder to
use, and one that does not cover a phone has nowhere to put a keyboard.
h-dvh rather than h-screen, so the sheet tracks the visual viewport when
the keyboard opens; env(safe-area-inset-bottom) on the composer, so the
send button is not under a home indicator.
The composer's input type is the server's decision, not a guess from what
the assistant said - see VisitorField. That is what puts an @ on the
keyboard when an email is wanted and lets iOS offer a verification code
straight from the notification.
The times are drawn from the tool result, never the prose
A turn that looked up availability carries the slots as structured data, and
the buttons come from that. So a time the visitor taps is one the server
offered, which is the property /visitor/book relies on and cannot check for
itself.
Replies render as text, not markdown
<ConnectAskAi> lazy-loads a markdown renderer because it answers questions
about a workspace's documents, where tables and code blocks carry meaning.
This answers three-sentence questions about a product on a marketing page.
Shipping 462 KB of renderer to a landing page for prose that is deliberately
kept to a couple of lines is the wrong trade, so line breaks are preserved
and nothing else is parsed.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectVisitorChatProps |
Returns
Element | null
Function: ConnectWorkspaceSwitcher()
function ConnectWorkspaceSwitcher(__namedParameters: Readonly<ConnectWorkspaceSwitcherProps>): Element | null;Switches which workspace this application is looking at.
Rendered in the AppShell sidebar header by default, directly
under the brand - which workspace you are in scopes everything below it, so
it reads as a heading for the nav rather than as one more destination.
It decides for itself whether to appear: an account with one workspace is
shown nothing by default, because a switcher offering one option teaches
the user nothing and costs a row of chrome on every screen. That is the
opposite of Connect's own switcher, which always renders - Connect is where
a workspace is created, so hiding it at one workspace would guarantee
nobody ever reached two. alwaysShow is the escape hatch for a partner
application that wants that same discoverability without becoming Connect:
it keeps the dropdown open below two workspaces and adds a row that opens
<ConnectCreateWorkspaceModal> inline, auto-switching into the result when
that is possible - see handleCreated below.
Authorization is the data, not a prop. The list comes from the caller's
own session (GET /workspaces takes no user id), so a workspace the user
may not reach is simply not in it. There is deliberately no roles prop to
pass or forget - the only way to render a row is for Connect to have
returned it.
Rows the user belongs to but cannot use here - a workspace outside the
application's audience, or one with no subscription - are shown disabled
with the reason rather than hidden. Hiding them reads as data loss to the
person who owns the workspace; switch refuses the same two cases, so the
label and the outcome always agree.
A switch rotates the session token. The bridge persists the new one in the
host application's own session store and the token never reaches this
component - so the only thing to do afterwards is reload, which is what
onSwitched defaults to.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectWorkspaceSwitcherProps> |
Returns
Element | null
Function: ConnectWorkspaceTypeGate()
function ConnectWorkspaceTypeGate(__namedParameters: ConnectWorkspaceTypeGateProps): Element;Renders children only for a workspace of the given type.
<ConnectWorkspaceTypeGate type="business">
<TeamMembersTable />
</ConnectWorkspaceTypeGate>The point is dead controls, not access: an "individual" workspace has
exactly one seat and can never have another, so a members table, an invite
button and a seat meter are all things that can only ever fail there.
Hiding them is a better answer than letting someone press one and read an
error about an account shape they did not choose.
This is emphatically not the audience gate. It does not know what your
application is sold to and will happily render a business-only subtree to a
business workspace that has no business being in your application at all -
that is <ConnectAccessGate>'s job, off the audience_mismatch readiness
blocker. Use this one inside an application both audiences can reach, to
vary what each sees.
Like every gate in this package it is a UX gate, not a security boundary: the browser can ignore it. Anything that must not happen for an individual workspace has to be refused on your server too.
Renders loading - not fallback - while the session is unknown, so a
business account never flickers through the individual-account layout.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | ConnectWorkspaceTypeGateProps |
Returns
Element
Function: dayColumns()
function dayColumns(
anchor: Date,
timeZone: string,
count: number,
now: Date): DayColumn[];count consecutive days starting at the local day anchor falls on.
Days are stepped by adding 24 hours and re-reading the zone rather than by
incrementing a date field, so a day that is 23 or 25 hours long still
produces the right sequence of dateKeys - the midday anchor is what keeps
a DST shift from landing the step on the previous or next day.
Parameters
| Parameter | Type |
|---|---|
anchor | Date |
timeZone | string |
count | number |
now | Date |
Returns
DayColumn[]
Function: discountLabel()
function discountLabel(discount: AppliedDiscount): string;Parameters
| Parameter | Type |
|---|---|
discount | AppliedDiscount |
Returns
string
Function: filterNavForAudience()
function filterNavForAudience(nav: ConnectNav, isStaff: boolean): ConnectNav;nav with every item the current session should not see removed - see
ConnectNavItem.audience.
Applied to nested items too, and a parent whose children all disappear is
dropped along with them: a section header pointing at nothing reads as a
broken link rather than as a hidden one.
Parameters
| Parameter | Type |
|---|---|
nav | ConnectNav |
isStaff | boolean |
Returns
Function: flattenNav()
function flattenNav(nav: ConnectNav): ConnectNavItem[];Every item in the tree, parents and children alike, in render order.
Parameters
| Parameter | Type |
|---|---|
nav | ConnectNav |
Returns
Function: formatAiValue()
function formatAiValue(value: AiValue, format?: AiValueFormat): string;Parameters
| Parameter | Type | Default value |
|---|---|---|
value | AiValue | undefined |
format | AiValueFormat | "text" |
Returns
string
Function: formatPrice()
function formatPrice(paise: number): string;Paise are the wire unit everywhere in Connect; rupees are the display unit.
Parameters
| Parameter | Type |
|---|---|
paise | number |
Returns
string
Function: gateExemptPaths()
function gateExemptPaths(nav: ConnectNav): string[];Hrefs declared gateExempt - the routes AppShell renders outside its ConnectAccessGate.
Parameters
| Parameter | Type |
|---|---|
nav | ConnectNav |
Returns
string[]
Function: groupPlansByTier()
function groupPlansByTier(plans: readonly Plan[]): PlanTier[];Collapses a flat plan list into one card per tier.
This is the shape the grid wants and the old one didn't have. It used to filter the whole list down to the toggled interval, which meant a tier sold monthly-only simply vanished when a customer looked at annual pricing, and a catalog with no monthly plans at all rendered an empty grid on first paint. Grouping first means the toggle picks a price inside a card, and a card with only one cadence still shows.
Ordered by the tier's cheapest monthly-equivalent price, so the grid reads left-to-right cheapest-first regardless of what order the API returned.
Parameters
| Parameter | Type |
|---|---|
plans | readonly Plan[] |
Returns
PlanTier[]
Function: isFreePlan()
function isFreePlan(price: number, addonsTotal?: number): boolean;Whether a plan costs nothing - the SDK's copy of Connect's isFreeSelection
(packages/data/src/payments/free-plan.ts), which is what the server
actually decides the free-activation path with.
A copy rather than an import for the reason types.ts gives: the published
package carries none of Connect's internals. The rule is one line and does
not drift, and conformance.test.ts pins the shape either side of it.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
price | number | undefined | - |
addonsTotal | number | 0 | Paise of the add-ons selected, not of the plan's whole catalog. A free plan with a paid add-on ticked is not free. |
Returns
boolean
Function: isLinkableConnectScreen()
function isLinkableConnectScreen(screens: readonly ConnectScreen[], href: string): boolean;Whether an href the model produced may be rendered as a link.
Same reasoning as Connect's own showActions check, and the same conclusion:
an assistant that can emit an arbitrary URL is a redirect the model can be
talked into, and retrieved content shares a context window with its
tool-calling authority. So a link is drawn only when it resolves to a screen
your application declared - which makes an external destination structurally
impossible, since no route pattern has a scheme or a host.
The protocol-relative form is refused separately: //evil.example.com has no
scheme for a path matcher to trip over, and would otherwise reach an anchor
as an absolute destination.
Parameters
| Parameter | Type |
|---|---|
screens | readonly ConnectScreen[] |
href | string |
Returns
boolean
Function: isNavItemActive()
function isNavItemActive(item: ConnectNavItem, activeHref: string | null): boolean;Whether item is the destination resolveActivePath picked.
Not just normalizePath(item.href) === activeHref, which is the trap this
exists to close: normalizePath strips the origin off an absolute URL, so
an external item pointed at https://docs.example.com normalizes to / and
lights up on every app's home route. External items are never active.
Parameters
| Parameter | Type |
|---|---|
item | ConnectNavItem |
activeHref | string | null |
Returns
boolean
Function: isNumericAiFormat()
function isNumericAiFormat(format?: AiValueFormat): boolean;Numeric columns read far better right-aligned; everything else stays with the text.
Parameters
| Parameter | Type | Default value |
|---|---|---|
format | AiValueFormat | "text" |
Returns
boolean
Function: layoutBookings()
function layoutBookings(
bookings: Booking[],
timeZone: string,
window: GridWindow): PositionedBooking[];Places bookings on the grid, side by side where they collide.
Overlap handling is the part worth stating. Two appointments at the same time are not an error - a workspace with two hosts has them constantly - and stacking them would hide one entirely. So overlapping bookings are grouped into clusters and each cluster is split into equal columns. The cluster is built transitively: A overlapping B and B overlapping C puts all three in one cluster even when A and C do not touch, because anything else lets two of them land on the same column.
Parameters
| Parameter | Type |
|---|---|
bookings | Booking[] |
timeZone | string |
window | GridWindow |
Returns
Function: matchConnectScreen()
function matchConnectScreen(screens: readonly ConnectScreen[], path: string): ConnectScreen | null;The declared screen a live path is on, or null if none matches.
null is a normal outcome, not an error: an application need only describe
the screens it wants the assistant to know about, and an undescribed one
simply costs the assistant its orientation rather than costing the user an
answer.
Parameters
| Parameter | Type |
|---|---|
screens | readonly ConnectScreen[] |
path | string |
Returns
ConnectScreen | null
Function: monthlyOf()
function monthlyOf(price: number, billingInterval: BillingInterval): number;An annual plan's price is 12 cycles of the same product, so any comparison across billing intervals has to normalise first - otherwise every annual plan reads as an upgrade over every monthly one.
Parameters
| Parameter | Type |
|---|---|
price | number |
billingInterval | BillingInterval |
Returns
number
Function: normalizeNav()
function normalizeNav(nav: ConnectNav): ConnectNavSection[];Folds a mixed ConnectNav into sections, gathering consecutive bare items into one untitled group.
Parameters
| Parameter | Type |
|---|---|
nav | ConnectNav |
Returns
Function: normalizePath()
function normalizePath(path: string): string;https://x.test/billing/?tab=1#top -> /billing.
Trailing slashes go too, so /billing/ and /billing are the same route -
they are, to every router, and treating them as different is how a nav item
ends up unhighlighted on exactly one of the two URLs a user can arrive at.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
string
Function: readVisitorStream()
function readVisitorStream(response: Response, handlers: VisitorStreamHandlers): Promise<void>;Parameters
| Parameter | Type |
|---|---|
response | Response |
handlers | VisitorStreamHandlers |
Returns
Promise<void>
Function: resolveAction()
function resolveAction(plan: Plan, current:
| SubscriptionResponse
| null): PlanAction;Parameters
| Parameter | Type |
|---|---|
plan | Plan |
current | | SubscriptionResponse | null |
Returns
Function: resolveActivePath()
function resolveActivePath(pathname: string, nav: ConnectNav): string | null;The href of the nav item the current path belongs to, or null.
Longest prefix wins, so /settings/billing lights up Billing and not also
its parent Settings - a plain startsWith over an unordered list lights up
both, and the sidebar grows two highlights.
External items never match: they leave the app, so no path inside it is ever "on" them.
Parameters
| Parameter | Type |
|---|---|
pathname | string |
nav | ConnectNav |
Returns
string | null
Function: resolvePlanGroup()
function resolvePlanGroup(plan: ComparablePlan): string;Falls back to a slug of the name for a Connect deployment predating
planGroup on the wire, mirroring resolvePlanGroup server-side. Kept so
an SDK upgrade doesn't require the partner's Connect instance to be
upgraded in lockstep - an older server simply omits the field and the grid
groups the way it always did.
Parameters
| Parameter | Type |
|---|---|
plan | ComparablePlan |
Returns
string
Function: resolveSubscriptionState()
function resolveSubscriptionState(subscription:
| SubscriptionResponse
| null, isTrialing: boolean): ConnectSubscriptionState;Parameters
| Parameter | Type |
|---|---|
subscription | | SubscriptionResponse | null |
isTrialing | boolean |
Returns
Function: sidebarRowClass()
function sidebarRowClass(options?: Readonly<{
tile?: boolean;
active?: boolean;
className?: string;
}>): string;The class list for one sidebar row, in one place.
Three different elements need it - a plain <button> (ConnectSidebarRow),
a rendered link (ConnectSidebarNav), and a HeroUI <Button> acting as a
Modal/Dropdown trigger (the account row, the launcher, the workspace
tile). They cannot share a component, because a React Aria overlay has to
own its own trigger element - so they share this instead. A second copy of
the string is a row that drifts off the 32px centre and takes the collapse
animation's smoothness with it.
On the HeroUI trigger: these are all Tailwind utilities, and HeroUI's
.button is a component class, so the utility layer wins - which is what
lets a w-fit, centred, px--padded button take this shape at all.
Parameters
| Parameter | Type |
|---|---|
options? | Readonly<{ tile?: boolean; active?: boolean; className?: string; }> |
Returns
string
Function: useActivePath()
function useActivePath(activePath?: string): string;The current path, from activePath when given and from the browser
otherwise.
Exported because a host building its own shell out of ConnectSideNav
needs the same fallback the shell uses.
Parameters
| Parameter | Type |
|---|---|
activePath? | string |
Returns
string
Function: useAvailabilitySchedules()
function useAvailabilitySchedules(): {
schedules: AvailabilitySchedule[];
isLoading: boolean;
error: string | null;
};The working-hours templates an appointment type can point at.
Read this before offering a "set up availability" step - a workspace that already has one does not need a second "Working hours" from your UI.
Returns
{
schedules: AvailabilitySchedule[];
isLoading: boolean;
error: string | null;
}schedules
schedules: AvailabilitySchedule[];isLoading
isLoading: boolean;error
error: string | null;Function: useAvailableSlots()
function useAvailableSlots(query: SlotQuery | null): {
slots: BookingSlot[];
timezone: string | null;
durationMinutes: number | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
};Free times for one appointment type.
Deliberately re-read rather than cached across a session. A slot list is a snapshot of contention: anyone else on that calendar - a colleague, a voice agent mid-call, someone on the public booking link - can take one between the read and the click. Showing a stale one is how two people are offered the same minute. The booking call re-checks inside a transaction anyway, so a stale list costs a refusal rather than a double booking; this just means the refusal is rare.
Parameters
| Parameter | Type |
|---|---|
query | SlotQuery | null |
Returns
slots
slots: BookingSlot[];timezone
timezone: string | null;durationMinutes
durationMinutes: number | null;isLoading
isLoading: boolean;error
error: string | null;refetch
refetch: () => void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useBookableEventTypes()
function useBookableEventTypes(): {
eventTypes: BookableEventType[];
isLoading: boolean;
error: string | null;
};What this workspace offers as bookable. Active types only.
Returns
{
eventTypes: BookableEventType[];
isLoading: boolean;
error: string | null;
}eventTypes
eventTypes: BookableEventType[];isLoading
isLoading: boolean;error
error: string | null;Function: useBookingActions()
function useBookingActions(): BookingActionsState & {
createBooking: Promise<Booking | null>;
rescheduleBooking: Promise<Booking | null>;
cancelBooking: Promise<Booking | null>;
};Book, move and cancel.
None of these is optimistic, and that is not an oversight. Whether a slot is still free is a question only the server can answer, inside the transaction that decides it - two people can be holding the same minute right now. Painting a booking as made and withdrawing it is exactly the lie the two-phase hold exists to prevent.
Returns
BookingActionsState & {
createBooking: Promise<Booking | null>;
rescheduleBooking: Promise<Booking | null>;
cancelBooking: Promise<Booking | null>;
}
Function: useBookings()
function useBookings(query: BookingsQuery): {
bookings: Booking[];
hasMore: boolean;
isLoading: boolean;
error: string | null;
path: string;
refetch: () => void;
};One page of the diary, ascending - a forward view, not a history.
Parameters
| Parameter | Type |
|---|---|
query | BookingsQuery |
Returns
bookings
bookings: Booking[];hasMore
hasMore: boolean;isLoading
isLoading: boolean;error
error: string | null;path
path: string;refetch
refetch: () => void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnect()
function useConnect(): ConnectContextValue;Browser half of @cosmoops/connect-sdk - React components and hooks.
Holds no credential. Everything here talks to your own origin, where
createConnectHandler from @cosmoops/connect-sdk/server holds the API key
and reaches Connect.
Styling is HeroUI v3 semantic tokens only (accent, muted, surface,
separator, ...), never a hardcoded colour - so these components inherit
whatever theme the host application platform already configured, and one theme
change lands identically in all of them.
Returns
Function: useConnectAccessGate()
function useConnectAccessGate(): ConnectAccessGateResult;The single composed answer to "can this workspace use my app right now, and if not, which of the four things is it": choose a plan, wait on approval, resolve an overdue payment, or - a suspended workspace - none of the above, because nothing short of a platform admin fixes that one.
Reuses the existing single sources of truth rather than re-deriving them:
blockingBlockers() (helpers.ts) for severity, the same readiness read
ConnectReadinessGate/ConnectReadinessBanner already use. ConnectAccessGate
is a priority ordering over the same data, not a second opinion about it.
Priority, first match wins: suspended > audience_mismatch >
no_subscription > onboarding_pending > any other blocking code
(blocked) > payment_retry (advisory - children still render) >
clear.
audience_mismatch outranks no_subscription deliberately, and the
ordering is the whole point of it: a workspace of the wrong account type
carries BOTH blockers at once, and showing the subscription one first sent
the customer to a plan picker whose subscribe call
(ActivateSubscriptionUseCase) rejects them for the audience anyway. The
account type is the reason; the missing subscription is a consequence.
Returns
Function: useConnectAddonUsage()
function useConnectAddonUsage(): {
data: | AddonUsageResponse
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};What is left of every consumable add-on on this workspace's subscription.
The read half of client.billing.consumeAddon - an application that meters its
own end users had no way to show them what remained until this existed.
Empty addons means there is nothing to meter, which is not an error.
Returns
data
data:
| AddonUsageResponse
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectApps()
function useConnectApps(): {
data: | {
apps: WorkspaceApp[];
}
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};The apps this workspace can open.
Returns
data
data:
| {
apps: WorkspaceApp[];
}
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectAudience()
function useConnectAudience(): SessionAudience | null;Whether this account is the kind your application is sold to - null while
the session is loading or signed out.
audience.matches === false is a hard block, not a hint: readiness reports
audience_mismatch and subscribing is refused. <ConnectAccessGate>
already blocks the shell on it, so reach for this only when you need the
verdict somewhere the gate does not run - a marketing page, say, or a
sign-up CTA you would rather not show to an account that cannot complete it.
Returns
SessionAudience | null
Function: useConnectCall()
function useConnectCall(): ConnectCallContextValue;Start a call yourself. Must be inside <ConnectCallProvider>.
Returns
Function: useConnectCheckout()
function useConnectCheckout(): ConnectCheckoutState & {
start: void;
reset: void;
};Drives a checkout that completes on Connect, not here.
The partner's page cannot complete a subscription itself - see
ConnectPlans' doc comment for why that is deliberate - so the flow is:
open Connect, watch /entitlements until the entitlement changes, then
refresh the session so every Connect component on the page repaints.
The result is read from Connect's own state, never from the window. A closed window proves nothing: the customer may have paid and closed it, or opened it, thought better of it, and closed it. Polling is what tells the two apart, which is why the window closing only starts a short grace period rather than ending the run.
Returns
Function: useConnectCompanyProfile()
function useConnectCompanyProfile(): {
data: | CompanyProfileResponse
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};The platform's legal identity and DPDP grievance channel.
The one read here that does not depend on a session, so it resolves the same for a signed-out visitor - which is deliberate, because the moment a grievance channel most needs to be readable is on a public privacy page.
Read it rather than copying it into your own configuration; see
CompanyProfile. <ConnectGrievanceNotice> renders it for you.
Returns
data
data:
| CompanyProfileResponse
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectEntitlements()
function useConnectEntitlements(): {
data: | {
readiness: ReadinessResponse;
subscription: SubscriptionResponse;
}
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};Readiness gates and the live subscription, fetched on first use.
Returns
data
data:
| {
readiness: ReadinessResponse;
subscription: SubscriptionResponse;
}
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectFeature()
function useConnectFeature(feature: string): {
isEntitled: boolean;
isLoading: boolean;
features: string[];
};Whether the live subscription unlocks a plan feature.
The browser twin of client.entitlements.hasFeature on the server, and the
same posture: false rather than a throw when there is no subscription at
all, because "not entitled" is the same answer either way and a feature check
should not need a try/catch around it.
isLoading is returned separately and matters: false while the first read
is in flight means "we don't know yet", not "no". Gate on isEntitled for
what to render and on isLoading for whether to render anything at all -
ConnectFeatureGate does exactly that.
Parameters
| Parameter | Type |
|---|---|
feature | string |
Returns
isEntitled
isEntitled: boolean;isLoading
isLoading: boolean;features
features: string[];Every feature key the current plan unlocks - for a settings page listing what's included.
Function: useConnectIntegrations()
function useConnectIntegrations(): {
data: | IntegrationsListResponse
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};Enabled Composio-hosted toolkits and this workspace's connection status for each.
Returns
data
data:
| IntegrationsListResponse
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectOptional()
function useConnectOptional(): ConnectContextValue | null;The context when there is one, null when there is not.
For a component that must work on a page with no provider, which is a
real case rather than defensive coding: <ConnectVisitorChat> goes on a
public marketing page, and those pages frequently have no ConnectProvider
anywhere near them - there is no session to hold and nothing on the page that
needs one. useConnect() throwing is right for every component that reads a
session; it is wrong for one whose whole purpose is the visitor who has none.
What inheriting the context buys such a component, when it is inside one, is
basePath and the injected fetch - so the same component renders against
Storybook's mock bridge and the docs site's canned fetch without either of
them having to pass anything.
Do not reach for this to make an ordinary component tolerant of a missing provider. A session-reading component that silently renders a signed-out state because somebody forgot to wrap their app is a bug that looks like a product decision.
Returns
ConnectContextValue | null
Function: useConnectPlans()
function useConnectPlans(): {
data: PlansResponse | null;
isLoading: boolean;
error: string | null;
refetch: void;
};This application's plan catalog.
Returns
data
data: PlansResponse | null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectPlatformAccess()
function useConnectPlatformAccess(): {
isStaff: boolean;
roles: string[];
isLoading: boolean;
};Whether this session is one of Connect's own operators - see
SessionAccess.staff.
isStaff is the branch you want; roles narrows further when your operator
console has tiers of its own (a developer seeing a read-only view of what
a platform_admin can edit, say).
A rendering decision, not an access decision. The browser can call this
whatever it likes. Everything an operator console exposes must be re-checked
server-side against staff from
client.identity.getUser(sessionToken) - see <ConnectPlatformGate>, which
says the same thing at more length.
Returns
isStaff
isStaff: boolean;roles
roles: string[];The staff role, as a one-element array.
Still an array because <ConnectPlatformGate accept={[...]}> matches
against a set, and because a session reports one role since
role/permissions/staff replaced the old platformAccess.roles
list - Connect's platform roles nest, so the highest one is the whole
answer. Empty for a member session.
isLoading
isLoading: boolean;true while the session read is in flight - neither staff nor not-staff yet.
Function: useConnectPortalUrl()
function useConnectPortalUrl(path: string): string;connectPortalUrl, with appUrl/clientId read from the provider instead
of threaded through by every caller.
Parameters
| Parameter | Type |
|---|---|
path | string |
Returns
string
Function: useConnectPush()
function useConnectPush(serviceWorkerUrl?: string): UseConnectPush;Register this browser for the platform's notifications.
The subscription is Connect's, not your application's: a member who turns notifications on in one app has turned them on for the platform, and the same device receives every other app's notifications and Connect's own. That is the point of centralising it - one browser prompt instead of one per app, and one place to turn everything off.
Your application needs a service worker at its own origin that handles the
push event; the SDK does not install one, because a worker is a
site-wide concern with caching implications only the host can decide.
Parameters
| Parameter | Type | Default value | Description |
|---|---|---|---|
serviceWorkerUrl | string | "/sw.js" | Where your worker lives. Root-scoped by convention. |
Returns
Function: useConnectResource()
function useConnectResource<T>(path: string, options?: Readonly<UseConnectResourceOptions>): {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: void;
};One bridge read, shared through the provider's cache.
Waits for a session by default: every resource route but one needs it, so
firing before one exists just buys a guaranteed 401. requiresSession: false opts out, and the reason it exists is the grievance notice - a
statutory contact is at its most useful on a public page a signed-out
visitor is reading, so a hook that answered null until someone logged in
would render nothing at exactly the moment that matters.
Type Parameters
| Type Parameter |
|---|
T |
Parameters
| Parameter | Type |
|---|---|
path | string |
options? | Readonly<UseConnectResourceOptions> |
Returns
data
data: T | null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectSessions()
function useConnectSessions(): {
data: | SessionsListResponse
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};This Connect account's own login sessions - account-wide, not scoped to
this application, see SessionsListResponse's doc comment. The component
that calls this is also the one that must own any revoke actions and call
the returned refetch() afterward - see mutateResource's doc comment
for why nothing does that automatically.
Returns
data
data:
| SessionsListResponse
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectShell()
function useConnectShell(): ConnectShellContextValue;The shell state a sidebar part is rendering inside.
Falls back to "expanded, not collapsible, plain anchors" rather than
throwing, because every part is also exported for use outside a shell -
a host dropping ConnectSidebarNav into a layout it already owns should
not have to mount a provider to get a nav column.
Returns
Function: useConnectUser()
function useConnectUser(): ConnectUser | null;The signed-in user, or null. Sugar over useConnect().session.
Returns
ConnectUser | null
Function: useConnectWorkspaceMembers()
function useConnectWorkspaceMembers(): {
data: | {
members: ConnectWorkspaceMember[];
}
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};The team of the workspace this session is on, with each member's permissions already resolved for this application.
Returns
data
data:
| {
members: ConnectWorkspaceMember[];
}
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectWorkspaces()
function useConnectWorkspaces(): {
data: | {
workspaces: UserWorkspace[];
}
| null;
isLoading: boolean;
error: string | null;
refetch: void;
};The workspaces this user belongs to - what <ConnectWorkspaceSwitcher>
renders.
Empty or single-entry results are normal, not errors: most accounts have one workspace, and the switcher decides for itself whether that is worth drawing.
Returns
data
data:
| {
workspaces: UserWorkspace[];
}
| null;isLoading
isLoading: boolean;error
error: string | null;refetch()
refetch(): void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useConnectWorkspaceType()
function useConnectWorkspaceType(): WorkspaceType | null;Whether the signed-in workspace is a one-seat personal account or a team -
null while the session is loading or signed out.
Branch on this to stop rendering team UI at a solo account: a members
table, an invite button and a seat meter are all dead controls on an
"individual" workspace, which has exactly one seat and cannot gain
another. <ConnectWorkspaceTypeGate> is the declarative form.
Returns
WorkspaceType | null
Function: useCreateAvailabilitySchedule()
function useCreateAvailabilitySchedule(): BookingActionsState & {
createSchedule: Promise<
| AvailabilitySchedule
| null>;
};Gives a workspace its first working hours.
Not optimistic - same posture as useBookingActions: this is one of
the two writes a host's first-run setup makes, and a modal that closes on
a failed create is worse than one that stays open with the reason shown.
Returns
BookingActionsState & {
createSchedule: Promise<
| AvailabilitySchedule
| null>;
}
Function: useCreateEventType()
function useCreateEventType(): BookingActionsState & {
createEventType: Promise<
| ManageableEventType
| null>;
};Creates the first thing a workspace can be booked for.
Host is a specific member, never a team - there is no groups read in this bridge, so a round-robin team host stays a Console-only capability reachable by following "Manage in Connect".
Returns
BookingActionsState & {
createEventType: Promise<
| ManageableEventType
| null>;
}
Function: useCreateWorkspace()
function useCreateWorkspace(): WorkspaceActionsState & {
createWorkspace: Promise<UserWorkspace | null>;
};Opens another workspace under the tenant the signed-in user already owns.
Not optimistic, same posture as every write in use-scheduling.ts:
whether the tenant's plan still has room for another workspace is a
question only the server can answer, and a modal that closes before that
answer comes back is the lie a two-phase write exists to avoid.
The result is a UserWorkspace row with isActive/subscribed both
false - creating does not switch the session to it, and the workspace has
no subscription yet. Pair with <ConnectWorkspaceSwitcher>'s own
mutateResource("/workspaces/switch", ...) call to move into it, guarded
on inAudience the same way that component's selectable() already is.
Returns
WorkspaceActionsState & {
createWorkspace: Promise<UserWorkspace | null>;
}
Function: useManageableEventTypes()
function useManageableEventTypes(): {
eventTypes: ManageableEventType[];
isLoading: boolean;
error: string | null;
refetch: () => void;
};The richer read ConnectEventTypeFormModal's edit mode needs - everything
useBookableEventTypes's booking catalogue deliberately withholds. See
the bridge route's own doc comment for why this is a separate read rather
than a wider response on that one. Includes inactive rows.
Returns
eventTypes
eventTypes: ManageableEventType[];isLoading
isLoading: boolean;error
error: string | null;refetch
refetch: () => void;Forces a fresh read - evicts this path's cache entry and re-runs the read.
It used only to re-trigger the effect, on the assumption that the caller
had just evicted the entry itself through mutateResource. That made it
useless for the other reason to refetch: something changed somewhere
this tab cannot see. The provider's cache is a Map of resolved
promises with no expiry, so a refetch() without the eviction re-read
the same settled promise and reported the same stale answer forever -
which is what left <ConnectAccessGate> showing "Choose a plan" in a tab
whose workspace had subscribed in a different one.
Still safe after a mutateResource against the same path: evicting an
entry that is already gone is a no-op.
Returns
void
Function: useUpdateAvailabilitySchedule()
function useUpdateAvailabilitySchedule(): BookingActionsState & {
updateSchedule: Promise<
| AvailabilitySchedule
| null>;
};Edits an existing availability schedule. Every field optional - send only what changed.
Returns
BookingActionsState & {
updateSchedule: Promise<
| AvailabilitySchedule
| null>;
}
Function: useUpdateEventType()
function useUpdateEventType(): BookingActionsState & {
updateEventType: Promise<
| ManageableEventType
| null>;
};Edits an existing appointment type. Every field optional - send only what
changed. hostMemberId omitted leaves the host untouched, including a
group host this surface cannot represent.
Returns
BookingActionsState & {
updateEventType: Promise<
| ManageableEventType
| null>;
}
Function: visitorSlotOfferFrom()
function visitorSlotOfferFrom(toolCalls: readonly VisitorToolCall[]): VisitorSlotOffer | null;The times a turn looked up, lifted out of its tool calls.
This is what the buttons are drawn from, never the assistant's sentence. A
time the visitor taps is then one the server offered, which is what makes it
impossible to book an appointment at a time nobody gave - the property
/visitor/book depends on and cannot check for itself.
null when the turn looked nothing up, and also when the lookup refused -
a tool answering ok: false is the assistant being told there is nothing
available, and it says so in prose. Rendering an empty slot list under that
would be a second, emptier way of saying the same thing.
Parameters
| Parameter | Type |
|---|---|
toolCalls | readonly VisitorToolCall[] |
Returns
VisitorSlotOffer | null
Function: zonedInstant()
function zonedInstant(instant: Date, timeZone: string): ZonedInstant;An instant, as the local day and minute it lands on in timeZone.
Built from formatToParts rather than by adding a fixed offset. An offset
is only correct until a DST boundary, and "add 5.5 hours" is wrong the
moment the same code is pointed at a zone that observes one - which is
exactly the kind of bug that appears twice a year and never in a test.
Parameters
| Parameter | Type |
|---|---|
instant | Date |
timeZone | string |
Returns
ZonedInstant