@cosmoops/connect-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +446 -0
  3. package/dist/cjs/cli/bin.js +16 -0
  4. package/dist/cjs/cli/init.js +179 -0
  5. package/dist/cjs/client/components/account-sessions.js +112 -0
  6. package/dist/cjs/client/components/apps-grid.js +46 -0
  7. package/dist/cjs/client/components/auth-buttons.js +42 -0
  8. package/dist/cjs/client/components/feature-gate.js +61 -0
  9. package/dist/cjs/client/components/link-button.js +24 -0
  10. package/dist/cjs/client/components/logo.js +27 -0
  11. package/dist/cjs/client/components/plan-pricing.js +166 -0
  12. package/dist/cjs/client/components/plans-page.js +39 -0
  13. package/dist/cjs/client/components/plans.js +212 -0
  14. package/dist/cjs/client/components/readiness.js +117 -0
  15. package/dist/cjs/client/components/subscribe-dialog.js +60 -0
  16. package/dist/cjs/client/components/subscription.js +189 -0
  17. package/dist/cjs/client/components/usage-meter.js +68 -0
  18. package/dist/cjs/client/components/user-menu.js +172 -0
  19. package/dist/cjs/client/index.js +99 -0
  20. package/dist/cjs/client/mark.js +16 -0
  21. package/dist/cjs/client/provider.js +259 -0
  22. package/dist/cjs/client/use-checkout.js +173 -0
  23. package/dist/cjs/errors.js +74 -0
  24. package/dist/cjs/format-relative-time.js +18 -0
  25. package/dist/cjs/helpers.js +30 -0
  26. package/dist/cjs/index.js +22 -0
  27. package/dist/cjs/next/index.js +311 -0
  28. package/dist/cjs/package.json +3 -0
  29. package/dist/cjs/server/client.js +114 -0
  30. package/dist/cjs/server/handler.js +333 -0
  31. package/dist/cjs/server/http.js +160 -0
  32. package/dist/cjs/server/index.js +18 -0
  33. package/dist/cjs/server/resources/access.js +28 -0
  34. package/dist/cjs/server/resources/ai.js +36 -0
  35. package/dist/cjs/server/resources/apps.js +30 -0
  36. package/dist/cjs/server/resources/billing.js +71 -0
  37. package/dist/cjs/server/resources/drive.js +39 -0
  38. package/dist/cjs/server/resources/entitlements.js +49 -0
  39. package/dist/cjs/server/resources/identity.js +194 -0
  40. package/dist/cjs/server/resources/logs.js +41 -0
  41. package/dist/cjs/server/resources/otp.js +29 -0
  42. package/dist/cjs/server/resources/sessions.js +82 -0
  43. package/dist/cjs/types.js +15 -0
  44. package/dist/esm/cli/bin.js +13 -0
  45. package/dist/esm/cli/init.js +174 -0
  46. package/dist/esm/client/components/account-sessions.js +109 -0
  47. package/dist/esm/client/components/apps-grid.js +43 -0
  48. package/dist/esm/client/components/auth-buttons.js +38 -0
  49. package/dist/esm/client/components/feature-gate.js +57 -0
  50. package/dist/esm/client/components/link-button.js +21 -0
  51. package/dist/esm/client/components/logo.js +24 -0
  52. package/dist/esm/client/components/plan-pricing.js +156 -0
  53. package/dist/esm/client/components/plans-page.js +36 -0
  54. package/dist/esm/client/components/plans.js +209 -0
  55. package/dist/esm/client/components/readiness.js +113 -0
  56. package/dist/esm/client/components/subscribe-dialog.js +57 -0
  57. package/dist/esm/client/components/subscription.js +181 -0
  58. package/dist/esm/client/components/usage-meter.js +65 -0
  59. package/dist/esm/client/components/user-menu.js +164 -0
  60. package/dist/esm/client/index.js +47 -0
  61. package/dist/esm/client/mark.js +13 -0
  62. package/dist/esm/client/provider.js +247 -0
  63. package/dist/esm/client/use-checkout.js +168 -0
  64. package/dist/esm/errors.js +69 -0
  65. package/dist/esm/format-relative-time.js +15 -0
  66. package/dist/esm/helpers.js +26 -0
  67. package/dist/esm/index.js +15 -0
  68. package/dist/esm/next/index.js +279 -0
  69. package/dist/esm/package.json +3 -0
  70. package/dist/esm/server/client.js +110 -0
  71. package/dist/esm/server/handler.js +330 -0
  72. package/dist/esm/server/http.js +156 -0
  73. package/dist/esm/server/index.js +10 -0
  74. package/dist/esm/server/resources/access.js +24 -0
  75. package/dist/esm/server/resources/ai.js +32 -0
  76. package/dist/esm/server/resources/apps.js +26 -0
  77. package/dist/esm/server/resources/billing.js +67 -0
  78. package/dist/esm/server/resources/drive.js +35 -0
  79. package/dist/esm/server/resources/entitlements.js +45 -0
  80. package/dist/esm/server/resources/identity.js +189 -0
  81. package/dist/esm/server/resources/logs.js +37 -0
  82. package/dist/esm/server/resources/otp.js +25 -0
  83. package/dist/esm/server/resources/sessions.js +78 -0
  84. package/dist/esm/types.js +14 -0
  85. package/dist/theme.css +84 -0
  86. package/dist/types/cli/bin.d.ts +1 -0
  87. package/dist/types/cli/init.d.ts +34 -0
  88. package/dist/types/client/components/account-sessions.d.ts +25 -0
  89. package/dist/types/client/components/apps-grid.d.ts +20 -0
  90. package/dist/types/client/components/auth-buttons.d.ts +31 -0
  91. package/dist/types/client/components/feature-gate.d.ts +57 -0
  92. package/dist/types/client/components/link-button.d.ts +20 -0
  93. package/dist/types/client/components/logo.d.ts +35 -0
  94. package/dist/types/client/components/plan-pricing.d.ts +96 -0
  95. package/dist/types/client/components/plans-page.d.ts +36 -0
  96. package/dist/types/client/components/plans.d.ts +34 -0
  97. package/dist/types/client/components/readiness.d.ts +52 -0
  98. package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
  99. package/dist/types/client/components/subscription.d.ts +123 -0
  100. package/dist/types/client/components/usage-meter.d.ts +24 -0
  101. package/dist/types/client/components/user-menu.d.ts +86 -0
  102. package/dist/types/client/index.d.ts +47 -0
  103. package/dist/types/client/mark.d.ts +11 -0
  104. package/dist/types/client/provider.d.ts +209 -0
  105. package/dist/types/client/use-checkout.d.ts +93 -0
  106. package/dist/types/errors.d.ts +62 -0
  107. package/dist/types/format-relative-time.d.ts +2 -0
  108. package/dist/types/helpers.d.ts +33 -0
  109. package/dist/types/index.d.ts +17 -0
  110. package/dist/types/next/index.d.ts +136 -0
  111. package/dist/types/server/client.d.ts +83 -0
  112. package/dist/types/server/handler.d.ts +67 -0
  113. package/dist/types/server/http.d.ts +52 -0
  114. package/dist/types/server/index.d.ts +15 -0
  115. package/dist/types/server/resources/access.d.ts +17 -0
  116. package/dist/types/server/resources/ai.d.ts +24 -0
  117. package/dist/types/server/resources/apps.d.ts +12 -0
  118. package/dist/types/server/resources/billing.d.ts +41 -0
  119. package/dist/types/server/resources/drive.d.ts +26 -0
  120. package/dist/types/server/resources/entitlements.d.ts +31 -0
  121. package/dist/types/server/resources/identity.d.ts +76 -0
  122. package/dist/types/server/resources/logs.d.ts +20 -0
  123. package/dist/types/server/resources/otp.d.ts +17 -0
  124. package/dist/types/server/resources/sessions.d.ts +37 -0
  125. package/dist/types/types.d.ts +434 -0
  126. package/package.json +102 -0
@@ -0,0 +1,68 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectUsageMeter = ConnectUsageMeter;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("@heroui/react");
7
+ const lucide_react_1 = require("lucide-react");
8
+ const provider_js_1 = require("../provider.js");
9
+ const plan_pricing_js_1 = require("./plan-pricing.js");
10
+ function formatUnits(value, unit) {
11
+ return `${value.toLocaleString("en-IN")} ${unit}`;
12
+ }
13
+ function formatDate(iso) {
14
+ return new Date(iso).toLocaleDateString(undefined, { day: "numeric", month: "short" });
15
+ }
16
+ /**
17
+ * How much of each consumable add-on this workspace has left.
18
+ *
19
+ * Reads the balance Connect actually debits, so the number here and the number
20
+ * `consumeAddon` enforces cannot disagree. Before the `addons:read` endpoint
21
+ * existed this component was impossible to write honestly: a service could
22
+ * spend an allowance down on every request and the only thing the API exposed
23
+ * was the list of add-on *ids* on the subscription.
24
+ *
25
+ * **It shows `balance` against `includedPerCycle`, and never a "used" figure.**
26
+ * With `carryForward` an unspent balance survives the cycle boundary, so
27
+ * `included - balance` can go negative - a meter that runs backwards. Real
28
+ * consumption for a cycle is a ledger question, and computing it from a
29
+ * balance would be a guess rendered as a fact.
30
+ */
31
+ function ConnectUsageMeter({ addonIds, hideWhenEmpty = true, title = "Usage this cycle", className, }) {
32
+ const { session } = (0, provider_js_1.useConnect)();
33
+ const { data, isLoading, error } = (0, provider_js_1.useConnectAddonUsage)();
34
+ if (!session?.user)
35
+ return null;
36
+ if (isLoading)
37
+ return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-40 rounded-xl ${className ?? ""}` });
38
+ if (error || !data)
39
+ return null;
40
+ // Defended here as well as in `billing.getAddonUsage`, and deliberately so:
41
+ // `data` is whatever the host app's bridge returned, which a partner can
42
+ // route, wrap or proxy. This component's contract is that it renders nothing
43
+ // when there is nothing to meter, and it should hold for a malformed body as
44
+ // much as for an empty list.
45
+ const available = data.addons ?? [];
46
+ const addons = addonIds
47
+ ? addonIds
48
+ .map((id) => available.find((addon) => addon.addonId === id))
49
+ .filter((addon) => !!addon)
50
+ : available;
51
+ if (addons.length === 0) {
52
+ if (hideWhenEmpty)
53
+ return null;
54
+ return ((0, jsx_runtime_1.jsx)(react_1.Card, { className: className, children: (0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: title }), (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: "No metered add-ons on this plan." })] }) }));
55
+ }
56
+ return ((0, jsx_runtime_1.jsxs)(react_1.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: title }), data.resetsAt && (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: ["Resets on ", formatDate(data.resetsAt), "."] })] }), (0, jsx_runtime_1.jsx)(react_1.Card.Content, { className: "flex flex-col gap-4", children: addons.map((addon) => ((0, jsx_runtime_1.jsx)(AddonRow, { addon: addon }, addon.addonId))) })] }));
57
+ }
58
+ function AddonRow({ addon }) {
59
+ const overdrawn = addon.balance < 0;
60
+ // Carried-forward balance can exceed the grant, so the bar is clamped at
61
+ // both ends rather than assuming `0 <= balance <= includedPerCycle`. An
62
+ // unclamped width renders as a bar overflowing its own track.
63
+ const fraction = addon.includedPerCycle > 0 ? Math.min(1, Math.max(0, addon.balance / addon.includedPerCycle)) : 0;
64
+ const carried = addon.balance > addon.includedPerCycle ? addon.balance - addon.includedPerCycle : 0;
65
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-baseline justify-between gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)("span", { className: "font-medium text-foreground", children: addon.name }), (0, jsx_runtime_1.jsxs)("span", { className: overdrawn ? "text-danger" : "text-muted", children: [overdrawn
66
+ ? `${formatUnits(Math.abs(addon.balance), addon.unit)} over`
67
+ : `${formatUnits(addon.balance, addon.unit)} left`, (0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [" of ", formatUnits(addon.includedPerCycle, addon.unit)] })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-surface", role: "meter", "aria-label": `${addon.name} remaining`, "aria-valuenow": addon.balance, "aria-valuemin": 0, "aria-valuemax": addon.includedPerCycle, children: (0, jsx_runtime_1.jsx)("div", { className: `h-full rounded-full ${overdrawn ? "bg-danger" : "bg-accent"}`, style: { width: `${overdrawn ? 100 : fraction * 100}%` } }) }), carried > 0 && ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: ["Includes ", formatUnits(carried, addon.unit), " carried over from last cycle."] })), overdrawn && addon.overagePricePerUnit !== null && ((0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-1.5 text-xs text-danger", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.AlertTriangle, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), "Billed at ", (0, plan_pricing_js_1.formatPrice)(addon.overagePricePerUnit), " per ", addon.unit, " beyond your allowance."] })), !overdrawn && addon.overagePolicy === "reject" && addon.balance === 0 && ((0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-1.5 text-xs text-danger", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.AlertTriangle, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), "Nothing left this cycle - further use is blocked until it resets."] }))] }));
68
+ }
@@ -0,0 +1,172 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectUserMenu = ConnectUserMenu;
5
+ exports.ConnectManageSubscriptionButton = ConnectManageSubscriptionButton;
6
+ exports.ConnectInvoicesButton = ConnectInvoicesButton;
7
+ exports.ConnectProfileButton = ConnectProfileButton;
8
+ exports.ConnectHomeButton = ConnectHomeButton;
9
+ exports.ConnectSignOutButton = ConnectSignOutButton;
10
+ const jsx_runtime_1 = require("react/jsx-runtime");
11
+ const react_1 = require("@heroui/react");
12
+ const lucide_react_1 = require("lucide-react");
13
+ const provider_js_1 = require("../provider.js");
14
+ const mark_js_1 = require("../mark.js");
15
+ const account_sessions_js_1 = require("./account-sessions.js");
16
+ const THEME_OPTIONS = [
17
+ { value: "light", label: "Light", icon: lucide_react_1.Sun },
18
+ { value: "dark", label: "Dark", icon: lucide_react_1.Moon },
19
+ { value: "system", label: "System", icon: lucide_react_1.Laptop },
20
+ ];
21
+ function initials(name, email) {
22
+ const source = name?.trim() || email?.trim() || "";
23
+ const parts = source.split(/[\s@._-]+/).filter(Boolean);
24
+ return (parts.slice(0, 2).map((part) => part[0]).join("") || "?").toUpperCase();
25
+ }
26
+ function openExternal(url) {
27
+ window.open(url, "_blank", "noopener,noreferrer");
28
+ }
29
+ /** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
30
+ function humanizeRole(role) {
31
+ const words = role.replace(/[_-]+/g, " ").trim();
32
+ return words.charAt(0).toUpperCase() + words.slice(1);
33
+ }
34
+ /**
35
+ * The subscription chip, and only when there is something to say.
36
+ *
37
+ * A plain "Active" badge is noise - it restates the default. A trial that is
38
+ * running out, a subscription paused mid-dunning, or one already cancelled
39
+ * are the three states where the account modal is exactly where a user goes
40
+ * looking, and the chip is what points them at Billing.
41
+ */
42
+ function subscriptionChip(subscription) {
43
+ if (!subscription)
44
+ return null;
45
+ if (subscription.isTrialing) {
46
+ return ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Trial" }));
47
+ }
48
+ if (subscription.status === "paused") {
49
+ return ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Paused" }));
50
+ }
51
+ if (subscription.status === "cancelled") {
52
+ return ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "danger", children: "Cancelled" }));
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * One of the three Connect destinations, as a tile rather than a menu row.
58
+ *
59
+ * These are not sibling actions of "Sign out" and should not queue up in the
60
+ * same vertical list as it - they are three places to *go*, all at another
61
+ * origin, and a row of equal-weight tiles says that at a glance where four
62
+ * stacked text buttons said "pick one of four unrelated things". The
63
+ * external-link mark stays on every one of them: they open in a new tab, and
64
+ * that is a promise the affordance has to make before the click, not after.
65
+ *
66
+ * `w-full` is load-bearing, not decoration: HeroUI's `.button` base is
67
+ * `w-fit`, so a tile dropped into a grid cell shrinks to its own label and
68
+ * the three of them come out ragged and unequal.
69
+ */
70
+ function DestinationTile({ icon, label, url, }) {
71
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className: "relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border bg-surface px-2 py-4 hover:bg-surface-secondary", onPress: () => openExternal(url), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "absolute end-2 top-2 size-3 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: "flex size-9 items-center justify-center rounded-full bg-surface-secondary text-muted", children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-foreground", children: label })] }));
72
+ }
73
+ /**
74
+ * Avatar and name in the trigger; an identity card, the three Connect
75
+ * destinations (Profile, Billing, Connect), an optional appearance control,
76
+ * this account's active sessions, and sign-out in the modal it opens.
77
+ *
78
+ * Profile/Billing/Connect open in a **new tab**, each marked with an
79
+ * external-link icon. They are a different application at a different
80
+ * origin, and navigating the host app away to reach them would lose
81
+ * whatever the user was doing here. Sign out is the only action that acts
82
+ * in place, which is why it sits alone in the footer rather than as a fourth
83
+ * item in the same list as three navigations.
84
+ *
85
+ * The session list is account-wide, not scoped to this service - see
86
+ * `ConnectUserSession`'s doc comment in `types.ts`. Appearance only renders
87
+ * when both `theme` and `onThemeChange` are supplied.
88
+ *
89
+ * Renders nothing when signed out - pair it with `<ConnectLoginButton>`, which
90
+ * renders nothing when signed *in*, and the two swap cleanly with no branch in
91
+ * your own layout.
92
+ *
93
+ * On the HeroUI shape: the trigger is a plain `<Button>` as a **direct child**
94
+ * of `<Modal>`, not wrapped in `<Modal.Trigger>`. `Dropdown.Trigger` (and by
95
+ * the same construction, `Modal.Trigger`) is itself a React Aria `Button`, so
96
+ * wrapping one produces a button inside a button - invalid markup, and press
97
+ * handling stops working. Kept uncontrolled (no `isOpen`/`onOpenChange`),
98
+ * matching `<ConnectAppsGrid>`'s own `<Modal>` - nothing here needs to close
99
+ * the modal programmatically; sign-out uses `slot="close"` instead.
100
+ */
101
+ function ConnectUserMenu({ compact = false, onSignOut, className, theme, onThemeChange, }) {
102
+ const { session, isLoading, signOut, appUrl } = (0, provider_js_1.useConnect)();
103
+ if (isLoading) {
104
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-8 shrink-0 rounded-full" }), !compact && (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-4 w-24" })] }));
105
+ }
106
+ const user = session?.user;
107
+ if (!user)
108
+ return null;
109
+ const label = user.displayName ?? user.email ?? "Account";
110
+ const role = session?.serviceRole?.role;
111
+ const planChip = subscriptionChip(session?.subscription ?? null);
112
+ return ((0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `Account: ${label}`, className: `gap-1 pl-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_1.Avatar, { size: "sm", color: "accent", variant: "soft", children: (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) }) }), !compact && (0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-foreground", children: label })] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { size: "md", scroll: "inside", children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { children: [(0, jsx_runtime_1.jsx)(react_1.Modal.CloseTrigger, {}), (0, jsx_runtime_1.jsx)(react_1.Modal.Header, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Heading, { children: "Account" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { className: "flex flex-col gap-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start gap-3 rounded-xl border border-border bg-surface-secondary p-4", children: [(0, jsx_runtime_1.jsx)(react_1.Avatar, { size: "lg", color: "accent", variant: "soft", children: (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsx)("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: user.email }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-2 flex flex-wrap items-center gap-1.5", children: [(0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", children: user.organizationName }), role && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && ((0, jsx_runtime_1.jsx)(react_1.Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-3 gap-2", children: [(0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4", "aria-hidden": "true" }), label: "Profile", url: `${appUrl}/profile` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: "size-4", "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: "size-4" }), label: "Connect", url: appUrl })] }), theme && onThemeChange && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
113
+ const [next] = [...keys];
114
+ if (typeof next === "string")
115
+ onThemeChange(next);
116
+ }, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => ((0, jsx_runtime_1.jsxs)(react_1.ToggleButton, { id: value, children: [index > 0 && (0, jsx_runtime_1.jsx)(react_1.ToggleButtonGroup.Separator, {}), (0, jsx_runtime_1.jsx)(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), (0, jsx_runtime_1.jsx)(react_1.Separator, {}), (0, jsx_runtime_1.jsx)(account_sessions_js_1.ConnectAccountSessions, {})] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { slot: "close", fullWidth: true, variant: "tertiary", className: "text-danger", onPress: () => {
117
+ void signOut().then(onSignOut);
118
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) })] }));
119
+ }
120
+ /**
121
+ * The "Manage subscription" action on its own, for layouts that want it
122
+ * visible rather than buried in the account menu - a billing banner, an empty
123
+ * state, a settings page.
124
+ */
125
+ function ConnectManageSubscriptionButton({ className }) {
126
+ const { appUrl } = (0, provider_js_1.useConnect)();
127
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () => openExternal(`${appUrl}/billing`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: "size-4 shrink-0" }), "Manage subscription", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
128
+ }
129
+ /**
130
+ * Invoices and payment history, on Connect.
131
+ *
132
+ * A deep link rather than a component that lists them, and that is not a
133
+ * placeholder: nothing in `/api/v1/service/*` exposes invoices, so an SDK-side
134
+ * list would have nothing to read. It is also the right answer regardless -
135
+ * a GST tax invoice is a legal document with a fixed format, downloadable and
136
+ * emailed from one place, and a partner-rendered copy of one would be a second
137
+ * version of a record that must have exactly one.
138
+ */
139
+ function ConnectInvoicesButton({ className }) {
140
+ const { session, appUrl } = (0, provider_js_1.useConnect)();
141
+ if (!session?.user)
142
+ return null;
143
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(`${appUrl}/billing/invoices`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ReceiptText, { className: "size-4 shrink-0" }), "Invoices", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
144
+ }
145
+ /**
146
+ * The "Profile" action on its own - the third of the three Connect
147
+ * destinations, alongside `ConnectManageSubscriptionButton` (Billing) and
148
+ * `ConnectHomeButton`.
149
+ *
150
+ * It was reachable only from inside `ConnectUserMenu`, which meant an app that
151
+ * builds its own account menu had to hardcode `${appUrl}/profile` to offer it.
152
+ */
153
+ function ConnectProfileButton({ className }) {
154
+ const { session, appUrl } = (0, provider_js_1.useConnect)();
155
+ if (!session?.user)
156
+ return null;
157
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(`${appUrl}/profile`), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
158
+ }
159
+ /** A plain "Open Connect" button, for a footer or a help menu. */
160
+ function ConnectHomeButton({ className }) {
161
+ const { appUrl } = (0, provider_js_1.useConnect)();
162
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openExternal(appUrl), children: [(0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: "size-4 shrink-0" }), "Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
163
+ }
164
+ /** Sign out on its own, for a settings page that wants it as a destructive row. */
165
+ function ConnectSignOutButton({ onSignOut, className, }) {
166
+ const { session, signOut } = (0, provider_js_1.useConnect)();
167
+ if (!session?.user)
168
+ return null;
169
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => {
170
+ void signOut().then(onSignOut);
171
+ }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4 shrink-0" }), "Sign out"] }));
172
+ }
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.resolvePlanGroup = exports.groupPlansByTier = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.useConnectFeature = exports.ConnectFeatureGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectAppsGrid = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
+ /**
6
+ * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
7
+ *
8
+ * Holds no credential. Everything here talks to your own origin, where
9
+ * `createConnectHandler` from `@cosmoops/connect-sdk/server` holds the API key
10
+ * and reaches Connect.
11
+ *
12
+ * Styling is HeroUI v3 semantic tokens only (`accent`, `muted`, `surface`,
13
+ * `separator`, ...), never a hardcoded colour - so these components inherit
14
+ * whatever theme the host service platform already configured, and one theme
15
+ * change lands identically in all of them.
16
+ */
17
+ var provider_js_1 = require("./provider.js");
18
+ Object.defineProperty(exports, "ConnectProvider", { enumerable: true, get: function () { return provider_js_1.ConnectProvider; } });
19
+ Object.defineProperty(exports, "useConnect", { enumerable: true, get: function () { return provider_js_1.useConnect; } });
20
+ Object.defineProperty(exports, "useConnectUser", { enumerable: true, get: function () { return provider_js_1.useConnectUser; } });
21
+ Object.defineProperty(exports, "useConnectEntitlements", { enumerable: true, get: function () { return provider_js_1.useConnectEntitlements; } });
22
+ Object.defineProperty(exports, "useConnectPlans", { enumerable: true, get: function () { return provider_js_1.useConnectPlans; } });
23
+ Object.defineProperty(exports, "useConnectApps", { enumerable: true, get: function () { return provider_js_1.useConnectApps; } });
24
+ Object.defineProperty(exports, "useConnectAddonUsage", { enumerable: true, get: function () { return provider_js_1.useConnectAddonUsage; } });
25
+ Object.defineProperty(exports, "useConnectSessions", { enumerable: true, get: function () { return provider_js_1.useConnectSessions; } });
26
+ Object.defineProperty(exports, "useConnectResource", { enumerable: true, get: function () { return provider_js_1.useConnectResource; } });
27
+ Object.defineProperty(exports, "CONNECT_APP_URL", { enumerable: true, get: function () { return provider_js_1.CONNECT_APP_URL; } });
28
+ // Re-exported from the root entry rather than kept there alone: it is a pure
29
+ // helper, but every caller is UI code, and making a component file reach two
30
+ // entry points for one function is friction with nothing behind it.
31
+ var helpers_js_1 = require("../helpers.js");
32
+ Object.defineProperty(exports, "blockingBlockers", { enumerable: true, get: function () { return helpers_js_1.blockingBlockers; } });
33
+ Object.defineProperty(exports, "normalizePlanFeatures", { enumerable: true, get: function () { return helpers_js_1.normalizePlanFeatures; } });
34
+ var errors_js_1 = require("../errors.js");
35
+ Object.defineProperty(exports, "ConnectError", { enumerable: true, get: function () { return errors_js_1.ConnectError; } });
36
+ Object.defineProperty(exports, "isConnectError", { enumerable: true, get: function () { return errors_js_1.isConnectError; } });
37
+ var mark_js_1 = require("./mark.js");
38
+ Object.defineProperty(exports, "ConnectMark", { enumerable: true, get: function () { return mark_js_1.ConnectMark; } });
39
+ var logo_js_1 = require("./components/logo.js");
40
+ Object.defineProperty(exports, "ConnectLogo", { enumerable: true, get: function () { return logo_js_1.ConnectLogo; } });
41
+ var auth_buttons_js_1 = require("./components/auth-buttons.js");
42
+ Object.defineProperty(exports, "ConnectLoginButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectLoginButton; } });
43
+ Object.defineProperty(exports, "ConnectRegisterButton", { enumerable: true, get: function () { return auth_buttons_js_1.ConnectRegisterButton; } });
44
+ var user_menu_js_1 = require("./components/user-menu.js");
45
+ Object.defineProperty(exports, "ConnectUserMenu", { enumerable: true, get: function () { return user_menu_js_1.ConnectUserMenu; } });
46
+ Object.defineProperty(exports, "ConnectManageSubscriptionButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectManageSubscriptionButton; } });
47
+ Object.defineProperty(exports, "ConnectInvoicesButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectInvoicesButton; } });
48
+ Object.defineProperty(exports, "ConnectProfileButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectProfileButton; } });
49
+ Object.defineProperty(exports, "ConnectHomeButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectHomeButton; } });
50
+ Object.defineProperty(exports, "ConnectSignOutButton", { enumerable: true, get: function () { return user_menu_js_1.ConnectSignOutButton; } });
51
+ // Built, compiled into `dist/`, and reachable from no entry point until now -
52
+ // `ConnectUserMenu` was the only thing that could render it.
53
+ var account_sessions_js_1 = require("./components/account-sessions.js");
54
+ Object.defineProperty(exports, "ConnectAccountSessions", { enumerable: true, get: function () { return account_sessions_js_1.ConnectAccountSessions; } });
55
+ var apps_grid_js_1 = require("./components/apps-grid.js");
56
+ Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
57
+ var plans_js_1 = require("./components/plans.js");
58
+ Object.defineProperty(exports, "ConnectPlans", { enumerable: true, get: function () { return plans_js_1.ConnectPlans; } });
59
+ var subscribe_dialog_js_1 = require("./components/subscribe-dialog.js");
60
+ Object.defineProperty(exports, "ConnectSubscribeDialog", { enumerable: true, get: function () { return subscribe_dialog_js_1.ConnectSubscribeDialog; } });
61
+ // The subscribe / upgrade / downgrade flow's two moving parts, exported so a
62
+ // partner can drive the same hand-off from its own pricing page instead of
63
+ // `<ConnectPlans>` - the pricing table is one presentation of the flow, not
64
+ // the flow itself.
65
+ // Subscription state, gates and the surfaces around the plan grid.
66
+ var subscription_js_1 = require("./components/subscription.js");
67
+ Object.defineProperty(exports, "ConnectSubscriptionGate", { enumerable: true, get: function () { return subscription_js_1.ConnectSubscriptionGate; } });
68
+ Object.defineProperty(exports, "ConnectNoSubscriptionCard", { enumerable: true, get: function () { return subscription_js_1.ConnectNoSubscriptionCard; } });
69
+ Object.defineProperty(exports, "ConnectSubscriptionCard", { enumerable: true, get: function () { return subscription_js_1.ConnectSubscriptionCard; } });
70
+ Object.defineProperty(exports, "ConnectTrialBanner", { enumerable: true, get: function () { return subscription_js_1.ConnectTrialBanner; } });
71
+ Object.defineProperty(exports, "ConnectSeatUsage", { enumerable: true, get: function () { return subscription_js_1.ConnectSeatUsage; } });
72
+ Object.defineProperty(exports, "resolveSubscriptionState", { enumerable: true, get: function () { return subscription_js_1.resolveSubscriptionState; } });
73
+ var readiness_js_1 = require("./components/readiness.js");
74
+ Object.defineProperty(exports, "ConnectReadinessBanner", { enumerable: true, get: function () { return readiness_js_1.ConnectReadinessBanner; } });
75
+ Object.defineProperty(exports, "ConnectReadinessGate", { enumerable: true, get: function () { return readiness_js_1.ConnectReadinessGate; } });
76
+ var feature_gate_js_1 = require("./components/feature-gate.js");
77
+ Object.defineProperty(exports, "ConnectFeatureGate", { enumerable: true, get: function () { return feature_gate_js_1.ConnectFeatureGate; } });
78
+ Object.defineProperty(exports, "useConnectFeature", { enumerable: true, get: function () { return feature_gate_js_1.useConnectFeature; } });
79
+ var usage_meter_js_1 = require("./components/usage-meter.js");
80
+ Object.defineProperty(exports, "ConnectUsageMeter", { enumerable: true, get: function () { return usage_meter_js_1.ConnectUsageMeter; } });
81
+ var plans_page_js_1 = require("./components/plans-page.js");
82
+ Object.defineProperty(exports, "ConnectPlansPage", { enumerable: true, get: function () { return plans_page_js_1.ConnectPlansPage; } });
83
+ var use_checkout_js_1 = require("./use-checkout.js");
84
+ Object.defineProperty(exports, "useConnectCheckout", { enumerable: true, get: function () { return use_checkout_js_1.useConnectCheckout; } });
85
+ Object.defineProperty(exports, "buildCheckoutUrl", { enumerable: true, get: function () { return use_checkout_js_1.buildCheckoutUrl; } });
86
+ var plan_pricing_js_1 = require("./components/plan-pricing.js");
87
+ Object.defineProperty(exports, "formatPrice", { enumerable: true, get: function () { return plan_pricing_js_1.formatPrice; } });
88
+ Object.defineProperty(exports, "monthlyOf", { enumerable: true, get: function () { return plan_pricing_js_1.monthlyOf; } });
89
+ Object.defineProperty(exports, "resolveAction", { enumerable: true, get: function () { return plan_pricing_js_1.resolveAction; } });
90
+ Object.defineProperty(exports, "annualSaving", { enumerable: true, get: function () { return plan_pricing_js_1.annualSaving; } });
91
+ Object.defineProperty(exports, "discountLabel", { enumerable: true, get: function () { return plan_pricing_js_1.discountLabel; } });
92
+ Object.defineProperty(exports, "ACTION_LABEL", { enumerable: true, get: function () { return plan_pricing_js_1.ACTION_LABEL; } });
93
+ // The tier model, exported because a partner writing its own pricing grid
94
+ // needs the same pairing `ConnectPlans` uses. Grouping a flat plan list by
95
+ // hand means matching on `name`, which Connect neither enforces unique nor
96
+ // keeps stable across a rename - `planGroup` is the field that exists to
97
+ // answer this, and `groupPlansByTier` is how to read it.
98
+ Object.defineProperty(exports, "groupPlansByTier", { enumerable: true, get: function () { return plan_pricing_js_1.groupPlansByTier; } });
99
+ Object.defineProperty(exports, "resolvePlanGroup", { enumerable: true, get: function () { return plan_pricing_js_1.resolvePlanGroup; } });
@@ -0,0 +1,16 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectMark = ConnectMark;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ /**
7
+ * The Connect mark - concentric rings on a solid accent badge.
8
+ *
9
+ * Painted from `--accent` / `--accent-foreground`, the same HeroUI tokens the
10
+ * host app themes, so the badge tracks whatever palette a service platform has
11
+ * configured rather than carrying a hardcoded CosmoOps green into someone
12
+ * else's brand.
13
+ */
14
+ function ConnectMark({ className = "size-5" }) {
15
+ return ((0, jsx_runtime_1.jsxs)("svg", { viewBox: "0 0 32 32", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", className: className, children: [(0, jsx_runtime_1.jsx)("rect", { width: "32", height: "32", rx: "9", fill: "var(--accent)" }), (0, jsx_runtime_1.jsxs)("g", { stroke: "var(--accent-foreground)", strokeWidth: "2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "16", cy: "16", r: "2.5" }), (0, jsx_runtime_1.jsx)("circle", { cx: "16", cy: "16", r: "6" }), (0, jsx_runtime_1.jsx)("circle", { cx: "16", cy: "16", r: "9.5" })] })] }));
16
+ }
@@ -0,0 +1,259 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CONNECT_APP_URL = void 0;
5
+ exports.ConnectProvider = ConnectProvider;
6
+ exports.useConnect = useConnect;
7
+ exports.useConnectUser = useConnectUser;
8
+ exports.useConnectEntitlements = useConnectEntitlements;
9
+ exports.useConnectAddonUsage = useConnectAddonUsage;
10
+ exports.useConnectPlans = useConnectPlans;
11
+ exports.useConnectApps = useConnectApps;
12
+ exports.useConnectSessions = useConnectSessions;
13
+ exports.useConnectResource = useConnectResource;
14
+ const jsx_runtime_1 = require("react/jsx-runtime");
15
+ const react_1 = require("react");
16
+ exports.CONNECT_APP_URL = "https://connect.cosmoops.com";
17
+ const ConnectContext = (0, react_1.createContext)(null);
18
+ /**
19
+ * Wraps the part of your app that shows Connect UI.
20
+ *
21
+ * Holds one session read, shared by every component below it, so a page with a
22
+ * user menu, an apps grid and a plan table makes one request rather than three.
23
+ * There is no API key here and there cannot be: everything goes to your own
24
+ * origin, where `createConnectHandler` holds the credential.
25
+ *
26
+ * ```tsx
27
+ * // Next.js - clientId comes from NEXT_PUBLIC_CONNECT_CLIENT_ID
28
+ * <ConnectProvider>
29
+ * <App />
30
+ * </ConnectProvider>
31
+ * ```
32
+ */
33
+ function ConnectProvider({ clientId = process.env.NEXT_PUBLIC_CONNECT_CLIENT_ID ?? "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, children, }) {
34
+ // One state cell, not three. `isLoading` is *derived* from "no result yet"
35
+ // rather than set - a `setIsLoading(true)` at the top of an effect is a
36
+ // synchronous setState inside that effect, which cascades an extra render
37
+ // and is what `react-hooks/set-state-in-effect` flags. Everything below
38
+ // writes state only from an async callback.
39
+ const [result, setResult] = (0, react_1.useState)(null);
40
+ const isLoading = result === null;
41
+ const session = result?.session ?? null;
42
+ const error = result?.error ?? null;
43
+ // One entry per path. Held in a ref, not state, because a cache write must
44
+ // not re-render every consumer of this context - only the component awaiting
45
+ // that promise cares.
46
+ const cache = (0, react_1.useRef)(new Map());
47
+ const fetchResource = (0, react_1.useCallback)((path) => {
48
+ const key = `${basePath}${path}`;
49
+ const existing = cache.current.get(key);
50
+ if (existing)
51
+ return existing;
52
+ const pending = (fetchImpl ?? globalThis.fetch)(key, {
53
+ credentials: "same-origin",
54
+ headers: { accept: "application/json" },
55
+ })
56
+ .then(async (response) => {
57
+ const body = await response.json().catch(() => null);
58
+ if (!response.ok) {
59
+ throw new Error(body?.error?.message ?? `Connect bridge returned ${response.status}`);
60
+ }
61
+ return body;
62
+ })
63
+ .catch((cause) => {
64
+ // A failed read must not be cached, or the UI can never recover
65
+ // without a reload.
66
+ cache.current.delete(key);
67
+ throw cause;
68
+ });
69
+ cache.current.set(key, pending);
70
+ return pending;
71
+ }, [basePath, fetchImpl]);
72
+ const mutateResource = (0, react_1.useCallback)(async (path, body) => {
73
+ const response = await (fetchImpl ?? globalThis.fetch)(`${basePath}${path}`, {
74
+ method: "POST",
75
+ credentials: "same-origin",
76
+ headers: {
77
+ accept: "application/json",
78
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
79
+ },
80
+ body: body !== undefined ? JSON.stringify(body) : undefined,
81
+ });
82
+ const responseBody = await response.json().catch(() => null);
83
+ if (!response.ok) {
84
+ throw new Error(responseBody?.error?.message ?? `Connect bridge returned ${response.status}`);
85
+ }
86
+ // Only this path's own entry - see this method's doc comment for why
87
+ // a blunt full-cache clear (like `refresh()`/`signOut()` use) is wrong
88
+ // here.
89
+ cache.current.delete(`${basePath}${path}`);
90
+ return responseBody;
91
+ }, [basePath, fetchImpl]);
92
+ const invalidateResource = (0, react_1.useCallback)((path) => {
93
+ cache.current.delete(`${basePath}${path}`);
94
+ }, [basePath]);
95
+ const refresh = (0, react_1.useCallback)(async () => {
96
+ cache.current.clear();
97
+ try {
98
+ const session = await fetchResource("/session");
99
+ setResult({ session, error: null });
100
+ }
101
+ catch (cause) {
102
+ // A bridge that cannot be reached is reported as signed out *with* an
103
+ // error, so a Login button still renders rather than the UI hanging on
104
+ // skeletons forever.
105
+ setResult({
106
+ session: { user: null, subscription: null, serviceRole: null },
107
+ error: cause instanceof Error ? cause.message : "Could not reach Connect",
108
+ });
109
+ }
110
+ }, [fetchResource]);
111
+ (0, react_1.useEffect)(() => {
112
+ // A documented false positive: `refresh` writes state only after awaiting
113
+ // the bridge, so nothing here is synchronous - but the rule cannot follow
114
+ // an await boundary across a `useCallback` and reports the reachable
115
+ // `setResult`. Fetching on mount is the sanctioned pattern; the rule
116
+ // targets the synchronous case, which this is not.
117
+ // eslint-disable-next-line react-hooks/set-state-in-effect
118
+ void refresh();
119
+ }, [refresh]);
120
+ const signIn = (0, react_1.useCallback)((options = {}) => {
121
+ const url = new URL("/sso/authorize", appUrl);
122
+ url.searchParams.set("client_id", clientId);
123
+ url.searchParams.set("response_type", "code");
124
+ url.searchParams.set("scope", "openid");
125
+ if (redirectUri)
126
+ url.searchParams.set("redirect_uri", redirectUri);
127
+ if (options.prompt)
128
+ url.searchParams.set("prompt", options.prompt);
129
+ // `state` is deliberately absent: it must be generated and stored
130
+ // server-side to be worth anything, so the redirect goes through your own
131
+ // start route, which mints one and forwards. See `ConnectLoginButton`.
132
+ window.location.href = redirectUri ? url.toString() : `${basePath}/signin${options.prompt ? "?prompt=create" : ""}`;
133
+ }, [appUrl, basePath, clientId, redirectUri]);
134
+ const signOut = (0, react_1.useCallback)(async () => {
135
+ await (fetchImpl ?? globalThis.fetch)(`${basePath}/logout`, {
136
+ method: "POST",
137
+ credentials: "same-origin",
138
+ }).catch(() => { });
139
+ cache.current.clear();
140
+ setResult({ session: { user: null, subscription: null, serviceRole: null }, error: null });
141
+ }, [basePath, fetchImpl]);
142
+ const value = (0, react_1.useMemo)(() => ({
143
+ clientId,
144
+ appUrl,
145
+ session,
146
+ isLoading,
147
+ error,
148
+ signIn,
149
+ signOut,
150
+ refresh,
151
+ fetchResource,
152
+ mutateResource,
153
+ invalidateResource,
154
+ }), [
155
+ clientId,
156
+ appUrl,
157
+ session,
158
+ isLoading,
159
+ error,
160
+ signIn,
161
+ signOut,
162
+ refresh,
163
+ fetchResource,
164
+ mutateResource,
165
+ invalidateResource,
166
+ ]);
167
+ return (0, jsx_runtime_1.jsx)(ConnectContext.Provider, { value: value, children: children });
168
+ }
169
+ function useConnect() {
170
+ const value = (0, react_1.useContext)(ConnectContext);
171
+ if (!value) {
172
+ throw new Error("useConnect must be used inside <ConnectProvider>. Wrap your app once, near the root.");
173
+ }
174
+ return value;
175
+ }
176
+ /** The signed-in user, or `null`. Sugar over `useConnect().session`. */
177
+ function useConnectUser() {
178
+ return useConnect().session?.user ?? null;
179
+ }
180
+ /** Readiness gates and the live subscription, fetched on first use. */
181
+ function useConnectEntitlements() {
182
+ return useConnectResource("/entitlements");
183
+ }
184
+ /**
185
+ * What is left of every consumable add-on on this workspace's subscription.
186
+ *
187
+ * The read half of `client.billing.consumeAddon` - a service that meters its
188
+ * own end users had no way to show them what remained until this existed.
189
+ * Empty `addons` means there is nothing to meter, which is not an error.
190
+ */
191
+ function useConnectAddonUsage() {
192
+ return useConnectResource("/addons");
193
+ }
194
+ /** This service's plan catalog. */
195
+ function useConnectPlans() {
196
+ return useConnectResource("/plans");
197
+ }
198
+ /** The apps this organization can open. */
199
+ function useConnectApps() {
200
+ return useConnectResource("/apps");
201
+ }
202
+ /**
203
+ * This Connect account's own login sessions - account-wide, not scoped to
204
+ * this service, see `SessionsListResponse`'s doc comment. The component
205
+ * that calls this is also the one that must own any revoke actions and call
206
+ * the returned `refetch()` afterward - see `mutateResource`'s doc comment
207
+ * for why nothing does that automatically.
208
+ */
209
+ function useConnectSessions() {
210
+ return useConnectResource("/sessions");
211
+ }
212
+ /**
213
+ * One bridge read, shared through the provider's cache.
214
+ *
215
+ * Only fetches once a user is known: every resource route needs a session, so
216
+ * firing before one exists just buys a guaranteed 401.
217
+ */
218
+ function useConnectResource(path) {
219
+ const { fetchResource, session, isLoading: sessionLoading } = useConnect();
220
+ // Stamped with the `path` it belongs to, so switching paths reports loading
221
+ // without a synchronous `setIsLoading(true)` - and, more usefully, so a
222
+ // resolved response for a previous path can never be read as this one's.
223
+ const [entry, setEntry] = (0, react_1.useState)(null);
224
+ const [version, setVersion] = (0, react_1.useState)(0);
225
+ (0, react_1.useEffect)(() => {
226
+ // Signed out is handled by derivation below rather than by writing state
227
+ // here - a synchronous setState inside an effect costs an extra render pass
228
+ // and is what `react-hooks/set-state-in-effect` exists to catch.
229
+ if (sessionLoading || !session?.user)
230
+ return;
231
+ let active = true;
232
+ fetchResource(path)
233
+ .then((data) => {
234
+ if (active)
235
+ setEntry({ path, data, error: null });
236
+ })
237
+ .catch((cause) => {
238
+ if (active) {
239
+ setEntry({ path, data: null, error: cause instanceof Error ? cause.message : "Request failed" });
240
+ }
241
+ });
242
+ return () => {
243
+ active = false;
244
+ };
245
+ // `version` is otherwise unread - it exists purely to force this effect
246
+ // to re-run on `refetch()`, after the caller has already evicted this
247
+ // path's cache entry via `mutateResource`.
248
+ }, [fetchResource, path, session?.user, sessionLoading, version]);
249
+ const signedOut = !sessionLoading && !session?.user;
250
+ const loaded = entry?.path === path ? entry : null;
251
+ return {
252
+ data: loaded?.data ?? null,
253
+ // Signed out is a settled answer, not a pending one - a gated component
254
+ // should render its signed-out state, not skeletons forever.
255
+ isLoading: !signedOut && !loaded,
256
+ error: loaded?.error ?? null,
257
+ refetch: () => setVersion((v) => v + 1),
258
+ };
259
+ }