@cosmoops/connect-sdk 0.3.0 → 0.6.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 (81) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +215 -6
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +162 -0
  6. package/dist/cjs/client/components/account-sessions.js +21 -19
  7. package/dist/cjs/client/components/app-shell.js +157 -0
  8. package/dist/cjs/client/components/apps-grid.js +2 -1
  9. package/dist/cjs/client/components/integrations-modal.js +95 -0
  10. package/dist/cjs/client/components/motion.js +27 -0
  11. package/dist/cjs/client/components/nav.js +97 -0
  12. package/dist/cjs/client/components/plans.js +39 -37
  13. package/dist/cjs/client/components/readiness.js +3 -1
  14. package/dist/cjs/client/components/side-nav.js +58 -0
  15. package/dist/cjs/client/components/subscription.js +2 -1
  16. package/dist/cjs/client/components/surfaces.js +54 -0
  17. package/dist/cjs/client/components/usage-meter.js +3 -2
  18. package/dist/cjs/client/components/user-menu.js +30 -3
  19. package/dist/cjs/client/index.js +29 -1
  20. package/dist/cjs/client/provider.js +54 -2
  21. package/dist/cjs/client/use-active-path.js +77 -0
  22. package/dist/cjs/errors.js +1 -1
  23. package/dist/cjs/next/index.js +96 -15
  24. package/dist/cjs/server/client.js +10 -2
  25. package/dist/cjs/server/handler.js +89 -6
  26. package/dist/cjs/server/index.js +8 -1
  27. package/dist/cjs/server/resources/analytics.js +66 -0
  28. package/dist/cjs/server/resources/identity.js +1 -1
  29. package/dist/cjs/server/resources/integrations.js +66 -0
  30. package/dist/cjs/server/resources/sessions.js +1 -1
  31. package/dist/cjs/server/webhooks.js +127 -0
  32. package/dist/esm/cli/doctor.js +3 -3
  33. package/dist/esm/cli/init.js +3 -3
  34. package/dist/esm/client/components/access-gate.js +158 -0
  35. package/dist/esm/client/components/account-sessions.js +21 -19
  36. package/dist/esm/client/components/app-shell.js +154 -0
  37. package/dist/esm/client/components/apps-grid.js +2 -1
  38. package/dist/esm/client/components/integrations-modal.js +92 -0
  39. package/dist/esm/client/components/motion.js +21 -0
  40. package/dist/esm/client/components/nav.js +89 -0
  41. package/dist/esm/client/components/plans.js +39 -37
  42. package/dist/esm/client/components/readiness.js +3 -1
  43. package/dist/esm/client/components/side-nav.js +53 -0
  44. package/dist/esm/client/components/subscription.js +2 -1
  45. package/dist/esm/client/components/surfaces.js +51 -0
  46. package/dist/esm/client/components/usage-meter.js +3 -2
  47. package/dist/esm/client/components/user-menu.js +31 -4
  48. package/dist/esm/client/index.js +15 -1
  49. package/dist/esm/client/provider.js +53 -2
  50. package/dist/esm/client/use-active-path.js +74 -0
  51. package/dist/esm/errors.js +1 -1
  52. package/dist/esm/next/index.js +95 -15
  53. package/dist/esm/server/client.js +10 -2
  54. package/dist/esm/server/handler.js +89 -6
  55. package/dist/esm/server/index.js +5 -0
  56. package/dist/esm/server/resources/analytics.js +62 -0
  57. package/dist/esm/server/resources/identity.js +1 -1
  58. package/dist/esm/server/resources/integrations.js +62 -0
  59. package/dist/esm/server/resources/sessions.js +1 -1
  60. package/dist/esm/server/webhooks.js +123 -0
  61. package/dist/theme.css +28 -0
  62. package/dist/types/client/components/access-gate.d.ts +79 -0
  63. package/dist/types/client/components/app-shell.d.ts +115 -0
  64. package/dist/types/client/components/integrations-modal.d.ts +21 -0
  65. package/dist/types/client/components/motion.d.ts +22 -0
  66. package/dist/types/client/components/nav.d.ts +110 -0
  67. package/dist/types/client/components/side-nav.d.ts +31 -0
  68. package/dist/types/client/components/surfaces.d.ts +51 -0
  69. package/dist/types/client/index.d.ts +12 -1
  70. package/dist/types/client/provider.d.ts +38 -3
  71. package/dist/types/client/use-active-path.d.ts +8 -0
  72. package/dist/types/errors.d.ts +1 -1
  73. package/dist/types/next/index.d.ts +40 -0
  74. package/dist/types/server/client.d.ts +8 -2
  75. package/dist/types/server/index.d.ts +7 -0
  76. package/dist/types/server/resources/analytics.d.ts +31 -0
  77. package/dist/types/server/resources/identity.d.ts +12 -2
  78. package/dist/types/server/resources/integrations.d.ts +40 -0
  79. package/dist/types/server/webhooks.d.ts +67 -0
  80. package/dist/types/types.d.ts +182 -1
  81. package/package.json +10 -4
@@ -8,6 +8,7 @@ const lucide_react_1 = require("lucide-react");
8
8
  const react_2 = require("react");
9
9
  const provider_js_1 = require("../provider.js");
10
10
  const use_checkout_js_1 = require("../use-checkout.js");
11
+ const motion_js_1 = require("./motion.js");
11
12
  const subscribe_dialog_js_1 = require("./subscribe-dialog.js");
12
13
  const plan_pricing_js_1 = require("./plan-pricing.js");
13
14
  /**
@@ -107,41 +108,41 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
107
108
  if (error) {
108
109
  return (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-danger", children: "Could not load plans." });
109
110
  }
110
- return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col gap-6 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [hasAnnual && hasMonthly && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("span", { className: showAnnual ? "text-sm text-muted" : "text-sm font-medium text-foreground", children: "Monthly" }), (0, jsx_runtime_1.jsx)(react_1.Switch, { "aria-label": "Bill annually", isSelected: showAnnual, onChange: setAnnual, children: (0, jsx_runtime_1.jsx)(react_1.Switch.Content, { children: (0, jsx_runtime_1.jsx)(react_1.Switch.Control, { children: (0, jsx_runtime_1.jsx)(react_1.Switch.Thumb, {}) }) }) }), (0, jsx_runtime_1.jsx)("span", { className: showAnnual ? "text-sm font-medium text-foreground" : "text-sm text-muted", children: "Annual" }), bestSaving !== null && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save up to ", bestSaving, "%"] }))] })), !hideCoupon && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Tag, { className: "size-4 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)(react_1.Input, { "aria-label": "Coupon code", placeholder: "Coupon code", value: coupon, onChange: (event) => setCoupon(event.target.value.toUpperCase()), className: "w-40" })] }))] }), subscription?.subscription?.appliedDiscount && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, plan_pricing_js_1.discountLabel)(subscription.subscription.appliedDiscount), subscription.subscription.appliedDiscount.code
111
- ? ` - code ${subscription.subscription.appliedDiscount.code}`
112
- : ""] })] })), (0, jsx_runtime_1.jsx)(ConnectCheckoutStatusRow, { checkout: checkout }), (0, jsx_runtime_1.jsx)("div", { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", children: tiers.map((tier) => {
113
- // The price point this card is showing. A tier missing the toggled
114
- // cadence falls back to the one it does sell rather than dropping
115
- // out of the grid, and says so under the price.
116
- const plan = pickTierPlan(tier, showAnnual);
117
- if (!plan)
118
- return null;
119
- const action = (0, plan_pricing_js_1.resolveAction)(plan, subscription);
120
- // "Current" belongs to the tier, not just the exact price point: an
121
- // org on Growth annual looking at monthly pricing is still on the
122
- // Growth tier, and marking only the row it literally subscribes to
123
- // would leave its own card unmarked half the time.
124
- const isCurrentTier = subscription?.plan != null &&
125
- (tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
126
- const isCurrent = action === "current";
127
- const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
128
- return ((0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "border-accent" : undefined, children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-muted", children: ["/", plan.billingInterval === "annual" ? "year" : "month"] }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] }), cadenceUnavailable && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", plan.billingInterval === "annual" ? "annually" : "monthly", "."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
129
- // `name`, not `key` - the key is an identifier
130
- // ("campaign-module"), and printing it here is what this
131
- // card used to do. The description rides along as a
132
- // title so a feature that needs explaining can have one
133
- // without a second line in every card.
134
- (0, jsx_runtime_1.jsxs)("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-foreground", children: [addon.name, addon.consumable && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "shrink-0 text-muted", children: (0, plan_pricing_js_1.formatPrice)(addon.price) })] }, addon.id)))] })), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [plan.seats.model === "unlimited" && "Unlimited seats", plan.seats.model === "fixed" && `${plan.seats.included} seats included`, plan.seats.model === "per-seat" &&
135
- `${plan.seats.included ?? 0} seats included, then ${(0, plan_pricing_js_1.formatPrice)(plan.seats.pricePerExtraSeat ?? 0)} each`] })] }), (0, jsx_runtime_1.jsx)(react_1.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { className: "w-full", variant: action === "upgrade" || action === "subscribe" || action === "switch-to-annual"
136
- ? "primary"
137
- : "secondary",
138
- // Disabled while a hand-off is in flight, not just on the
139
- // card that started it: the org has one subscription per
140
- // service, so a second checkout opened against a different
141
- // plan would race the first and one of them would be
142
- // rejected on arrival.
143
- isDisabled: isCurrent || checkout.status === "waiting", onPress: () => setPending({ plan, action }), children: [plan_pricing_js_1.ACTION_LABEL[action], !isCurrent && (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) })] }, tier.planGroup));
144
- }) }), subscription?.subscription && ((0, jsx_runtime_1.jsx)(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), (0, jsx_runtime_1.jsx)(subscribe_dialog_js_1.ConnectSubscribeDialog, { plan: pending?.plan ?? null, action: pending?.action ?? "subscribe", current: subscription, coupon: hideCoupon ? undefined : coupon, onClose: () => setPending(null), onConfirm: () => pending && startCheckout(pending.plan) })] }));
111
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col gap-6 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [hasAnnual && hasMonthly && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("span", { className: showAnnual ? "text-sm text-muted" : "text-sm font-medium text-foreground", children: "Monthly" }), (0, jsx_runtime_1.jsx)(react_1.Switch, { "aria-label": "Bill annually", isSelected: showAnnual, onChange: setAnnual, children: (0, jsx_runtime_1.jsx)(react_1.Switch.Content, { children: (0, jsx_runtime_1.jsx)(react_1.Switch.Control, { children: (0, jsx_runtime_1.jsx)(react_1.Switch.Thumb, {}) }) }) }), (0, jsx_runtime_1.jsx)("span", { className: showAnnual ? "text-sm font-medium text-foreground" : "text-sm text-muted", children: "Annual" }), bestSaving !== null && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save up to ", bestSaving, "%"] }))] })), !hideCoupon && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Tag, { className: "size-4 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)(react_1.Input, { "aria-label": "Coupon code", placeholder: "Coupon code", value: coupon, onChange: (event) => setCoupon(event.target.value.toUpperCase()), className: "w-40" })] }))] }), subscription?.subscription?.appliedDiscount && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { className: "size-4 shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("span", { children: [(0, plan_pricing_js_1.discountLabel)(subscription.subscription.appliedDiscount), subscription.subscription.appliedDiscount.code
112
+ ? ` - code ${subscription.subscription.appliedDiscount.code}`
113
+ : ""] })] })), (0, jsx_runtime_1.jsx)(ConnectCheckoutStatusRow, { checkout: checkout }), (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3", variants: { show: { transition: { staggerChildren: 0.06 } } }, initial: "hidden", animate: "show", children: tiers.map((tier) => {
114
+ // The price point this card is showing. A tier missing the toggled
115
+ // cadence falls back to the one it does sell rather than dropping
116
+ // out of the grid, and says so under the price.
117
+ const plan = pickTierPlan(tier, showAnnual);
118
+ if (!plan)
119
+ return null;
120
+ const action = (0, plan_pricing_js_1.resolveAction)(plan, subscription);
121
+ // "Current" belongs to the tier, not just the exact price point: an
122
+ // org on Growth annual looking at monthly pricing is still on the
123
+ // Growth tier, and marking only the row it literally subscribes to
124
+ // would leave its own card unmarked half the time.
125
+ const isCurrentTier = subscription?.plan != null &&
126
+ (tier.monthly?.id === subscription.plan.id || tier.annual?.id === subscription.plan.id);
127
+ const isCurrent = action === "current";
128
+ const cadenceUnavailable = showAnnual ? tier.annual === null : tier.monthly === null;
129
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { variants: { hidden: { opacity: 0, y: 12 }, show: { opacity: 1, y: 0, transition: motion_js_1.ENTER } }, children: (0, jsx_runtime_1.jsxs)(react_1.Card, { className: isCurrentTier ? "h-full border-accent" : "h-full", children: [(0, jsx_runtime_1.jsxs)(react_1.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: tier.name }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 items-center gap-1.5", children: [tier.annualSavingPercent !== null && showAnnual && ((0, jsx_runtime_1.jsxs)(react_1.Chip, { className: "bg-accent-soft text-accent", children: ["Save ", tier.annualSavingPercent, "%"] })), isCurrentTier && (0, jsx_runtime_1.jsx)(react_1.Chip, { className: "bg-accent-soft text-accent", children: "Current" })] })] }), tier.description && (0, jsx_runtime_1.jsx)(react_1.Card.Description, { children: tier.description })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "popLayout", initial: false, children: (0, jsx_runtime_1.jsxs)(motion_js_1.m.div, { initial: { opacity: 0, y: 6 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -6 }, transition: { duration: 0.15 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "text-2xl font-semibold text-foreground", children: (0, plan_pricing_js_1.formatPrice)(plan.price) }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-muted", children: ["/", plan.billingInterval === "annual" ? "year" : "month"] })] }, plan.billingInterval) }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["+", plan.gstRate, "% GST"] }), cadenceUnavailable && ((0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-xs text-muted", children: ["Only sold ", plan.billingInterval === "annual" ? "annually" : "monthly", "."] }))] }), plan.trialDays ? ((0, jsx_runtime_1.jsxs)("p", { className: "text-xs font-medium text-accent", children: [plan.trialDays, "-day free trial"] })) : null, plan.features.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: "flex flex-col gap-1.5", children: plan.features.map((feature) => (
130
+ // `name`, not `key` - the key is an identifier
131
+ // ("campaign-module"), and printing it here is what this
132
+ // card used to do. The description rides along as a
133
+ // title so a feature that needs explaining can have one
134
+ // without a second line in every card.
135
+ (0, jsx_runtime_1.jsxs)("li", { className: "flex items-start gap-2 text-sm text-foreground", title: feature.description || undefined, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "mt-0.5 size-4 shrink-0 text-accent", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: feature.name })] }, feature.key))) })), plan.addons.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1.5 border-t border-separator pt-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-xs font-medium uppercase tracking-wide text-muted", children: "Add-ons" }), plan.addons.map((addon) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-baseline justify-between gap-2 text-sm", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-foreground", children: [addon.name, addon.consumable && ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted", children: [" ", "- ", addon.consumable.includedPerCycle.toLocaleString("en-IN"), " ", addon.consumable.unit, "/cycle"] }))] }), (0, jsx_runtime_1.jsx)("span", { className: "shrink-0 text-muted", children: (0, plan_pricing_js_1.formatPrice)(addon.price) })] }, addon.id)))] })), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs text-muted", children: [plan.seats.model === "unlimited" && "Unlimited seats", plan.seats.model === "fixed" && `${plan.seats.included} seats included`, plan.seats.model === "per-seat" &&
136
+ `${plan.seats.included ?? 0} seats included, then ${(0, plan_pricing_js_1.formatPrice)(plan.seats.pricePerExtraSeat ?? 0)} each`] })] }), (0, jsx_runtime_1.jsx)(react_1.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(react_1.Button, { className: "w-full", variant: action === "upgrade" || action === "subscribe" || action === "switch-to-annual"
137
+ ? "primary"
138
+ : "secondary",
139
+ // Disabled while a hand-off is in flight, not just on the
140
+ // card that started it: the org has one subscription per
141
+ // service, so a second checkout opened against a different
142
+ // plan would race the first and one of them would be
143
+ // rejected on arrival.
144
+ isDisabled: isCurrent || checkout.status === "waiting", onPress: () => setPending({ plan, action }), children: [plan_pricing_js_1.ACTION_LABEL[action], !isCurrent && (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) })] }) }, tier.planGroup));
145
+ }) }), subscription?.subscription && ((0, jsx_runtime_1.jsx)(ConnectUnsubscribeRow, { subscription: subscription.subscription, appUrl: appUrl, checkout: checkout })), (0, jsx_runtime_1.jsx)(subscribe_dialog_js_1.ConnectSubscribeDialog, { plan: pending?.plan ?? null, action: pending?.action ?? "subscribe", current: subscription, coupon: hideCoupon ? undefined : coupon, onClose: () => setPending(null), onConfirm: () => pending && startCheckout(pending.plan) })] }) }));
145
146
  }
146
147
  /**
147
148
  * What the page says while the customer is off in Connect, and after.
@@ -153,8 +154,9 @@ function ConnectPlans({ planIds, hideCoupon = false, className }) {
153
154
  * read as one.
154
155
  */
155
156
  function ConnectCheckoutStatusRow({ checkout, }) {
156
- if (checkout.status === "idle")
157
- return null;
157
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.AnimatePresence, { mode: "wait", initial: false, children: checkout.status !== "idle" && ((0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: 4 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -4 }, transition: { duration: 0.18 }, children: (0, jsx_runtime_1.jsx)(CheckoutStatusRowContent, { checkout: checkout }) }, checkout.status)) }));
158
+ }
159
+ function CheckoutStatusRowContent({ checkout, }) {
158
160
  if (checkout.status === "waiting") {
159
161
  return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 rounded-lg border border-separator px-3 py-2 text-sm text-muted", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "size-4 shrink-0 animate-spin", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { children: "Finish up in the Connect window. This page updates on its own." })] }));
160
162
  }
@@ -8,6 +8,7 @@ const react_1 = require("@heroui/react");
8
8
  const lucide_react_1 = require("lucide-react");
9
9
  const helpers_js_1 = require("../../helpers.js");
10
10
  const provider_js_1 = require("../provider.js");
11
+ const motion_js_1 = require("./motion.js");
11
12
  /**
12
13
  * Codes that mean "somebody at Connect is looking at it" - there is no action
13
14
  * for the customer to take, so these get an informational treatment and no
@@ -26,6 +27,7 @@ const ACTION_LABELS = {
26
27
  verification_rejected: "Review verification",
27
28
  verification_expired: "Renew verification",
28
29
  seat_limit_reached: "Manage seats",
30
+ payment_retry_pending: "Resolve payment",
29
31
  };
30
32
  function actionLabel(blocker) {
31
33
  return ACTION_LABELS[blocker.code] ?? "Resolve on Connect";
@@ -66,7 +68,7 @@ function ConnectReadinessBanner({ includeAdvisory = false, hideWhenReady = true,
66
68
  const blockers = includeAdvisory ? data.readiness.readiness.blockers : (0, helpers_js_1.blockingBlockers)(data.readiness);
67
69
  if (blockers.length === 0)
68
70
  return hideWhenReady ? null : (0, jsx_runtime_1.jsx)(ReadyRow, { className: className });
69
- return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-2 ${className ?? ""}`, role: "status", children: blockers.map((blocker) => ((0, jsx_runtime_1.jsx)(BlockerRow, { blocker: blocker }, `${blocker.code}-${blocker.message}`))) }));
71
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { className: `flex flex-col gap-2 ${className ?? ""}`, role: "status", initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: motion_js_1.ENTER, children: blockers.map((blocker) => ((0, jsx_runtime_1.jsx)(BlockerRow, { blocker: blocker }, `${blocker.code}-${blocker.message}`))) }) }));
70
72
  }
71
73
  function BlockerRow({ blocker }) {
72
74
  const inProgress = IN_PROGRESS_CODES.has(blocker.code);
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.defaultRenderLink = void 0;
5
+ exports.ConnectSideNav = ConnectSideNav;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("@heroui/react");
8
+ const lucide_react_1 = require("lucide-react");
9
+ const use_active_path_js_1 = require("../use-active-path.js");
10
+ const nav_js_1 = require("./nav.js");
11
+ /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
12
+ const defaultRenderLink = ({ children, ...props }) => (0, jsx_runtime_1.jsx)("a", { ...props, children: children });
13
+ exports.defaultRenderLink = defaultRenderLink;
14
+ const ROW = "group relative flex items-center rounded-lg text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus";
15
+ function rowClass(isActive, collapsed, depth) {
16
+ const shape = collapsed ? "size-10 justify-center" : `min-h-9 justify-start gap-3 px-3 py-2 ${depth > 0 ? "ml-4" : ""}`;
17
+ const tone = isActive
18
+ ? "bg-accent-soft font-medium text-accent"
19
+ : "text-foreground hover:bg-surface-secondary";
20
+ return `${ROW} ${shape} ${tone}`;
21
+ }
22
+ function NavRow({ item, isActive, hasActiveChild, collapsed, depth, renderLink, onNavigate, }) {
23
+ const linkProps = {
24
+ href: item.href,
25
+ className: rowClass(isActive, collapsed, depth),
26
+ "aria-label": item.label,
27
+ "aria-current": isActive ? "page" : undefined,
28
+ onClick: onNavigate,
29
+ ...(item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}),
30
+ children: ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "relative flex size-5 shrink-0 items-center justify-center", children: item.icon }), !collapsed && (0, jsx_runtime_1.jsx)("span", { className: "relative flex-1 truncate", children: item.label }), !collapsed && item.badge, !collapsed && item.external && ((0, jsx_runtime_1.jsx)(lucide_react_1.ArrowUpRight, { className: "size-3.5 shrink-0 text-muted", "aria-hidden": "true" })), collapsed && hasActiveChild && !isActive && ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: "absolute bottom-1 size-1 rounded-full bg-accent" }))] })),
31
+ };
32
+ // The tooltip stays mounted across expand/collapse and only toggles
33
+ // `isDisabled`. Branching into two trees instead re-creates every item's
34
+ // overlay and focus machinery on each toggle, which is what makes the width
35
+ // animation stutter.
36
+ return ((0, jsx_runtime_1.jsxs)(react_1.Tooltip, { delay: 200, isDisabled: !collapsed, children: [(0, jsx_runtime_1.jsx)(react_1.Tooltip.Trigger, { "aria-label": item.label, children: renderLink(linkProps) }), (0, jsx_runtime_1.jsx)(react_1.Tooltip.Content, { placement: "right", children: item.label })] }));
37
+ }
38
+ /**
39
+ * The sidebar's nav column on its own - sections, items, one level of
40
+ * children, and the active highlight.
41
+ *
42
+ * `ConnectAppShell` renders this inside its rail and its mobile drawer, and it
43
+ * is exported for a host that wants the shell's navigation without its
44
+ * chrome: the same items, the same active resolution, dropped into a layout
45
+ * you already have.
46
+ */
47
+ function ConnectSideNav({ nav, activePath, renderLink = exports.defaultRenderLink, collapsed = false, onNavigate, className, }) {
48
+ const pathname = (0, use_active_path_js_1.useActivePath)(activePath);
49
+ const active = (0, nav_js_1.resolveActivePath)(pathname, nav);
50
+ const sections = (0, nav_js_1.normalizeNav)(nav);
51
+ return ((0, jsx_runtime_1.jsx)("div", { className: `flex flex-col gap-4 ${className ?? ""}`, children: sections.map((section, index) => ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": section.title ?? "Main", className: `flex flex-col gap-1 ${collapsed ? "items-center" : ""}`, children: [section.title && !collapsed && ((0, jsx_runtime_1.jsx)("h2", { className: "px-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted", children: section.title })), section.title && collapsed && index > 0 && (0, jsx_runtime_1.jsx)("span", { className: "my-1 h-px w-6 bg-separator", "aria-hidden": "true" }), section.items.map((item) => {
52
+ const isActive = (0, nav_js_1.isNavItemActive)(item, active);
53
+ const activeChild = (item.items ?? []).some((child) => (0, nav_js_1.isNavItemActive)(child, active));
54
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col gap-1 ${collapsed ? "" : "w-full"}`, children: [(0, jsx_runtime_1.jsx)(NavRow, { item: item, isActive: isActive, hasActiveChild: activeChild, collapsed: collapsed, depth: 0, renderLink: renderLink, onNavigate: onNavigate }), !collapsed &&
55
+ (isActive || activeChild) &&
56
+ item.items?.map((child) => ((0, jsx_runtime_1.jsx)(NavRow, { item: child, isActive: (0, nav_js_1.isNavItemActive)(child, active), hasActiveChild: false, collapsed: false, depth: 1, renderLink: renderLink, onNavigate: onNavigate }, child.href)))] }, item.href));
57
+ })] }, section.title ?? index))) }));
58
+ }
@@ -11,6 +11,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
11
11
  const react_1 = require("@heroui/react");
12
12
  const lucide_react_1 = require("lucide-react");
13
13
  const provider_js_1 = require("../provider.js");
14
+ const motion_js_1 = require("./motion.js");
14
15
  const plan_pricing_js_1 = require("./plan-pricing.js");
15
16
  const link_button_js_1 = require("./link-button.js");
16
17
  /** Where the plan catalogue lives in the host app. Overridable, because not every app mounts it at `/plans`. */
@@ -84,7 +85,7 @@ function ConnectSubscriptionCard({ plansHref, className }) {
84
85
  return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: `h-48 rounded-xl ${className ?? ""}` });
85
86
  if (!detail || !plan)
86
87
  return (0, jsx_runtime_1.jsx)(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
87
- 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.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", plan.billingInterval === "annual" ? "year" : "month"] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }));
88
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 }, children: (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.jsxs)("div", { className: "flex items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Card.Title, { children: plan.name }), (0, jsx_runtime_1.jsx)(SubscriptionChip, { state: state })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Description, { children: [(0, plan_pricing_js_1.formatPrice)(plan.price), " + ", plan.gstRate, "% GST per", " ", plan.billingInterval === "annual" ? "year" : "month"] })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Content, { className: "flex flex-col gap-2 text-sm", children: [(0, jsx_runtime_1.jsx)(NextEvent, { detail: detail, state: state }), (0, jsx_runtime_1.jsx)(ConnectSeatUsage, { plan: plan, detail: detail })] }), (0, jsx_runtime_1.jsxs)(react_1.Card.Footer, { className: "flex flex-wrap gap-2", children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => window.open(`${appUrl}/billing/subscriptions/${detail.id}`, "_blank", "noopener,noreferrer"), children: ["Manage on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }), (0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Change plan" })] })] }) }) }));
88
89
  }
89
90
  function SubscriptionChip({ state }) {
90
91
  switch (state) {
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ICON = exports.MEDALLION = exports.PANEL_FILL = void 0;
4
+ /**
5
+ * The one elevation model every panel inside a Connect modal follows.
6
+ *
7
+ * This exists because "just use `bg-surface`" is wrong here, and wrong in a
8
+ * way that is invisible until you look at the tokens. `Modal.Dialog` paints
9
+ * itself `bg-overlay`, and in the shared theme (`theme.css`) `--overlay` and
10
+ * `--surface` sit at the *same lightness* in both schemes - 100% against 100%
11
+ * in light, 21% against 21% in dark. A `bg-surface` panel dropped on a modal
12
+ * therefore has no fill contrast at all; only its border separates it from
13
+ * the dialog behind it.
14
+ *
15
+ * That is how the account modal ended up looking half-finished: the identity
16
+ * card used `bg-surface-secondary` and read as a solid block, while the
17
+ * destination tiles and the session list used `bg-surface` and read as
18
+ * hairline outlines floating on nothing. Same nesting depth, two different
19
+ * physical materials, for no reason a user could infer.
20
+ *
21
+ * Three levels, and nesting one inside another always steps the fill:
22
+ *
23
+ * - **L0 - the dialog itself.** `bg-overlay`, owned by HeroUI. Not ours.
24
+ * - **L1 - a panel on the dialog.** `PANEL_FILL` + `border-border`. The
25
+ * identity card, each destination tile, the session list.
26
+ * - **L2 - a medallion inside a panel.** `bg-surface`, which is L0's value
27
+ * and therefore reads as *inset* against L1 rather than raised. Light:
28
+ * white on grey. Dark: near-black on charcoal. Both directions legible.
29
+ *
30
+ * The inversion at L2 is deliberate and is why this is written down. Reaching
31
+ * for a "lighter" fill one step down works in light mode and quietly fails in
32
+ * dark; stepping *back to the dialog's own value* is the trick that reads in
33
+ * both, because the contrast comes from the gap between L1 and L0 rather than
34
+ * from a direction of travel.
35
+ */
36
+ exports.PANEL_FILL = "bg-surface-secondary";
37
+ /**
38
+ * The icon well shared by destination tiles and session rows: a 40px box
39
+ * holding a 20px glyph.
40
+ *
41
+ * Half the well, which is the point. Both surfaces used to run a `size-4`
42
+ * (16px) icon inside a `size-9` (36px) well - a 44% fill that reads as an
43
+ * icon someone forgot to finish, and the specific thing that made the
44
+ * Profile/Billing/Connect tiles look under-drawn. They also disagreed on
45
+ * shape (a circle in the tiles, a rounded square in the session rows) for
46
+ * what is the same component role, so the two surfaces never looked related.
47
+ *
48
+ * Pair with `ICON` on the glyph inside. Session rows override the well to
49
+ * `size-9` for density but keep the same `ICON`, so glyph size stays constant
50
+ * across the whole modal even where the well does not.
51
+ */
52
+ exports.MEDALLION = "size-10";
53
+ /** The glyph inside a `MEDALLION`. Constant across every well in the modal - see `MEDALLION`. */
54
+ exports.ICON = "size-5";
@@ -6,6 +6,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("@heroui/react");
7
7
  const lucide_react_1 = require("lucide-react");
8
8
  const provider_js_1 = require("../provider.js");
9
+ const motion_js_1 = require("./motion.js");
9
10
  const plan_pricing_js_1 = require("./plan-pricing.js");
10
11
  function formatUnits(value, unit) {
11
12
  return `${value.toLocaleString("en-IN")} ${unit}`;
@@ -53,7 +54,7 @@ function ConnectUsageMeter({ addonIds, hideWhenEmpty = true, title = "Usage this
53
54
  return null;
54
55
  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
  }
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
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (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
  }
58
59
  function AddonRow({ addon }) {
59
60
  const overdrawn = addon.balance < 0;
@@ -64,5 +65,5 @@ function AddonRow({ addon }) {
64
65
  const carried = addon.balance > addon.includedPerCycle ? addon.balance - addon.includedPerCycle : 0;
65
66
  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
67
  ? `${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
+ : `${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)(motion_js_1.m.div, { className: `h-full rounded-full ${overdrawn ? "bg-danger" : "bg-accent"}`, initial: { width: 0 }, animate: { width: `${overdrawn ? 100 : fraction * 100}%` }, transition: motion_js_1.METER_SPRING }) }), 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
69
  }
@@ -13,6 +13,7 @@ const lucide_react_1 = require("lucide-react");
13
13
  const provider_js_1 = require("../provider.js");
14
14
  const mark_js_1 = require("../mark.js");
15
15
  const account_sessions_js_1 = require("./account-sessions.js");
16
+ const surfaces_js_1 = require("./surfaces.js");
16
17
  const THEME_OPTIONS = [
17
18
  { value: "light", label: "Light", icon: lucide_react_1.Sun },
18
19
  { value: "dark", label: "Dark", icon: lucide_react_1.Moon },
@@ -26,6 +27,20 @@ function initials(name, email) {
26
27
  function openExternal(url) {
27
28
  window.open(url, "_blank", "noopener,noreferrer");
28
29
  }
30
+ /**
31
+ * The member's photo, with their initials underneath it.
32
+ *
33
+ * Both halves always render, which is the whole trick: `<Avatar.Fallback>` is
34
+ * what shows while `src` is still loading and what stays if it 404s or the
35
+ * user has no photo at all, so there is no `photoUrl &&` branch anywhere and
36
+ * no flash of an empty circle on a cold cache. This component previously
37
+ * rendered *only* the fallback - `ConnectUser` carried no photo field at all,
38
+ * so every Connect account showed initials no matter what picture the member
39
+ * had set on their profile.
40
+ */
41
+ function UserAvatar({ user, size, }) {
42
+ return ((0, jsx_runtime_1.jsxs)(react_1.Avatar, { size: size, color: "accent", variant: "soft", children: [user.photoUrl && (0, jsx_runtime_1.jsx)(react_1.Avatar.Image, { alt: "", src: user.photoUrl }), (0, jsx_runtime_1.jsx)(react_1.Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
43
+ }
29
44
  /** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
30
45
  function humanizeRole(role) {
31
46
  const words = role.replace(/[_-]+/g, " ").trim();
@@ -66,9 +81,17 @@ function subscriptionChip(subscription) {
66
81
  * `w-full` is load-bearing, not decoration: HeroUI's `.button` base is
67
82
  * `w-fit`, so a tile dropped into a grid cell shrinks to its own label and
68
83
  * the three of them come out ragged and unequal.
84
+ *
85
+ * On the box: `p-3` all round rather than the `px-2 py-4` this used to carry.
86
+ * That old pair was both lopsided (16px of vertical air against 8px of
87
+ * horizontal) and too tall for what it held, so a 16px glyph sat marooned in
88
+ * a ~92px tile. Even padding plus the shared `size-10`/`size-5` medallion
89
+ * (see `MEDALLION`) puts the icon at half the well instead of a stranded
90
+ * 44%, and takes the tile down to ~88px while making its contents read
91
+ * bigger, not smaller.
69
92
  */
70
93
  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 })] }));
94
+ 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 ${surfaces_js_1.PANEL_FILL} p-3 hover:border-accent 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.5 text-muted", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: `flex ${surfaces_js_1.MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-foreground", children: label })] }));
72
95
  }
73
96
  /**
74
97
  * Avatar and name in the trigger; an identity card, the three Connect
@@ -109,11 +132,15 @@ function ConnectUserMenu({ compact = false, onSignOut, className, theme, onTheme
109
132
  const label = user.displayName ?? user.email ?? "Account";
110
133
  const role = session?.serviceRole?.role;
111
134
  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) => {
135
+ return ((0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
136
+ // `pl-1` against the default right padding is optical, not sloppy: a
137
+ // circular avatar needs less inset than a text label to look evenly
138
+ // seated in the button.
139
+ className: `gap-2 pl-1 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "sm" }), !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 ${surfaces_js_1.PANEL_FILL} p-4`, children: [(0, jsx_runtime_1.jsx)(UserAvatar, { user: user, size: "lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", 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.jsx)("p", { className: "truncate text-xs text-muted", children: user.organizationName }), (role || planChip || !user.emailVerified) && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [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: surfaces_js_1.ICON, "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: surfaces_js_1.ICON, "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: surfaces_js_1.ICON }), 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
140
  const [next] = [...keys];
114
141
  if (typeof next === "string")
115
142
  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: () => {
143
+ }, 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)(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
144
  void signOut().then(onSignOut);
118
145
  }, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) })] }));
119
146
  }
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  "use strict";
3
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;
4
+ exports.ConnectFeatureGate = exports.useConnectAccessGate = exports.ConnectAccessGate = exports.ConnectReadinessGate = exports.ConnectReadinessBanner = exports.resolveSubscriptionState = exports.ConnectSeatUsage = exports.ConnectTrialBanner = exports.ConnectSubscriptionCard = exports.ConnectNoSubscriptionCard = exports.ConnectSubscriptionGate = exports.ConnectSubscribeDialog = exports.ConnectPlans = exports.ConnectIntegrationsModal = exports.ConnectAppsGrid = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useActivePath = exports.ConnectSideNav = exports.ConnectAppShell = 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.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectUser = exports.useConnect = exports.ConnectProvider = void 0;
5
+ 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 = void 0;
5
6
  /**
6
7
  * Browser half of `@cosmoops/connect-sdk` - React components and hooks.
7
8
  *
@@ -23,6 +24,7 @@ Object.defineProperty(exports, "useConnectPlans", { enumerable: true, get: funct
23
24
  Object.defineProperty(exports, "useConnectApps", { enumerable: true, get: function () { return provider_js_1.useConnectApps; } });
24
25
  Object.defineProperty(exports, "useConnectAddonUsage", { enumerable: true, get: function () { return provider_js_1.useConnectAddonUsage; } });
25
26
  Object.defineProperty(exports, "useConnectSessions", { enumerable: true, get: function () { return provider_js_1.useConnectSessions; } });
27
+ Object.defineProperty(exports, "useConnectIntegrations", { enumerable: true, get: function () { return provider_js_1.useConnectIntegrations; } });
26
28
  Object.defineProperty(exports, "useConnectResource", { enumerable: true, get: function () { return provider_js_1.useConnectResource; } });
27
29
  Object.defineProperty(exports, "CONNECT_APP_URL", { enumerable: true, get: function () { return provider_js_1.CONNECT_APP_URL; } });
28
30
  // Re-exported from the root entry rather than kept there alone: it is a pure
@@ -52,8 +54,31 @@ Object.defineProperty(exports, "ConnectSignOutButton", { enumerable: true, get:
52
54
  // `ConnectUserMenu` was the only thing that could render it.
53
55
  var account_sessions_js_1 = require("./components/account-sessions.js");
54
56
  Object.defineProperty(exports, "ConnectAccountSessions", { enumerable: true, get: function () { return account_sessions_js_1.ConnectAccountSessions; } });
57
+ // The frame the rest of these components live inside. Exported next to the
58
+ // pieces it composes rather than in a section of its own - an integration
59
+ // reaching for a sidebar wants to find the launcher and the account menu in
60
+ // the same breath, since the shell already mounts both.
61
+ var app_shell_js_1 = require("./components/app-shell.js");
62
+ Object.defineProperty(exports, "ConnectAppShell", { enumerable: true, get: function () { return app_shell_js_1.ConnectAppShell; } });
63
+ var side_nav_js_1 = require("./components/side-nav.js");
64
+ Object.defineProperty(exports, "ConnectSideNav", { enumerable: true, get: function () { return side_nav_js_1.ConnectSideNav; } });
65
+ var use_active_path_js_1 = require("./use-active-path.js");
66
+ Object.defineProperty(exports, "useActivePath", { enumerable: true, get: function () { return use_active_path_js_1.useActivePath; } });
67
+ // The nav model is plain data and pure functions - a host generating its nav
68
+ // from its own route table needs the types, and `resolveActivePath` is the
69
+ // same longest-prefix rule the sidebar highlights with, so a breadcrumb or a
70
+ // page title derived from it cannot disagree with the rail.
71
+ var nav_js_1 = require("./components/nav.js");
72
+ Object.defineProperty(exports, "normalizeNav", { enumerable: true, get: function () { return nav_js_1.normalizeNav; } });
73
+ Object.defineProperty(exports, "flattenNav", { enumerable: true, get: function () { return nav_js_1.flattenNav; } });
74
+ Object.defineProperty(exports, "normalizePath", { enumerable: true, get: function () { return nav_js_1.normalizePath; } });
75
+ Object.defineProperty(exports, "resolveActivePath", { enumerable: true, get: function () { return nav_js_1.resolveActivePath; } });
76
+ Object.defineProperty(exports, "isNavItemActive", { enumerable: true, get: function () { return nav_js_1.isNavItemActive; } });
77
+ Object.defineProperty(exports, "gateExemptPaths", { enumerable: true, get: function () { return nav_js_1.gateExemptPaths; } });
55
78
  var apps_grid_js_1 = require("./components/apps-grid.js");
56
79
  Object.defineProperty(exports, "ConnectAppsGrid", { enumerable: true, get: function () { return apps_grid_js_1.ConnectAppsGrid; } });
80
+ var integrations_modal_js_1 = require("./components/integrations-modal.js");
81
+ Object.defineProperty(exports, "ConnectIntegrationsModal", { enumerable: true, get: function () { return integrations_modal_js_1.ConnectIntegrationsModal; } });
57
82
  var plans_js_1 = require("./components/plans.js");
58
83
  Object.defineProperty(exports, "ConnectPlans", { enumerable: true, get: function () { return plans_js_1.ConnectPlans; } });
59
84
  var subscribe_dialog_js_1 = require("./components/subscribe-dialog.js");
@@ -73,6 +98,9 @@ Object.defineProperty(exports, "resolveSubscriptionState", { enumerable: true, g
73
98
  var readiness_js_1 = require("./components/readiness.js");
74
99
  Object.defineProperty(exports, "ConnectReadinessBanner", { enumerable: true, get: function () { return readiness_js_1.ConnectReadinessBanner; } });
75
100
  Object.defineProperty(exports, "ConnectReadinessGate", { enumerable: true, get: function () { return readiness_js_1.ConnectReadinessGate; } });
101
+ var access_gate_js_1 = require("./components/access-gate.js");
102
+ Object.defineProperty(exports, "ConnectAccessGate", { enumerable: true, get: function () { return access_gate_js_1.ConnectAccessGate; } });
103
+ Object.defineProperty(exports, "useConnectAccessGate", { enumerable: true, get: function () { return access_gate_js_1.useConnectAccessGate; } });
76
104
  var feature_gate_js_1 = require("./components/feature-gate.js");
77
105
  Object.defineProperty(exports, "ConnectFeatureGate", { enumerable: true, get: function () { return feature_gate_js_1.ConnectFeatureGate; } });
78
106
  Object.defineProperty(exports, "useConnectFeature", { enumerable: true, get: function () { return feature_gate_js_1.useConnectFeature; } });
@@ -9,12 +9,15 @@ exports.useConnectEntitlements = useConnectEntitlements;
9
9
  exports.useConnectAddonUsage = useConnectAddonUsage;
10
10
  exports.useConnectPlans = useConnectPlans;
11
11
  exports.useConnectApps = useConnectApps;
12
+ exports.useConnectIntegrations = useConnectIntegrations;
12
13
  exports.useConnectSessions = useConnectSessions;
13
14
  exports.useConnectResource = useConnectResource;
14
15
  const jsx_runtime_1 = require("react/jsx-runtime");
15
16
  const react_1 = require("react");
16
17
  exports.CONNECT_APP_URL = "https://connect.cosmoops.com";
17
18
  const ConnectContext = (0, react_1.createContext)(null);
19
+ /** Marks that this tab has already spent its one silent attempt - see `ConnectProviderProps.silentSignIn`. */
20
+ const SILENT_ATTEMPT_KEY = "connect_silent_attempted";
18
21
  /**
19
22
  * Wraps the part of your app that shows Connect UI.
20
23
  *
@@ -29,14 +32,44 @@ const ConnectContext = (0, react_1.createContext)(null);
29
32
  * </ConnectProvider>
30
33
  * ```
31
34
  */
32
- function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, children, }) {
35
+ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, silentSignIn = false, children, }) {
33
36
  // One state cell, not three. `isLoading` is *derived* from "no result yet"
34
37
  // rather than set - a `setIsLoading(true)` at the top of an effect is a
35
38
  // synchronous setState inside that effect, which cascades an extra render
36
39
  // and is what `react-hooks/set-state-in-effect` flags. Everything below
37
40
  // writes state only from an async callback.
38
41
  const [result, setResult] = (0, react_1.useState)(null);
39
- const isLoading = result === null;
42
+ /**
43
+ * Whether this tab has already spent its one silent attempt, sampled once
44
+ * at mount.
45
+ *
46
+ * A lazy `useState` initializer rather than a read inside the render body:
47
+ * `sessionStorage` is external mutable state, and reading it during render
48
+ * is impure - with the React Compiler on, that is exactly the kind of thing
49
+ * that can be hoisted or memoized into the wrong answer. An initializer
50
+ * runs once, which is all this needs; the value only ever changes as this
51
+ * component navigates the page away.
52
+ *
53
+ * Safe on the server, where it reads `false`: `result` is `null` during SSR
54
+ * and first client render alike, so `isLoading` below is `true` on both
55
+ * regardless of this value, and hydration cannot diverge on it.
56
+ */
57
+ const [silentAttemptSpent] = (0, react_1.useState)(() => typeof window !== "undefined" && window.sessionStorage.getItem(SILENT_ATTEMPT_KEY) === "1");
58
+ /**
59
+ * A silent attempt is settled-signed-out *and* about to redirect. Reported
60
+ * as still-loading, which is the honest answer: the question "is this
61
+ * visitor signed in" has not been finally answered yet, and there is a
62
+ * navigation already on its way.
63
+ *
64
+ * Without this the provider publishes `signed out` for the one frame before
65
+ * the effect below fires, so every consumer paints its signed-out state -
66
+ * a "Sign in" button, the plan-picker gate - and then the page vanishes
67
+ * underneath it. Flashing a sales pitch at someone who is about to be
68
+ * silently signed in is the same bug `ConnectSubscriptionGate` exists to
69
+ * prevent, one layer down.
70
+ */
71
+ const silentPending = silentSignIn && !silentAttemptSpent && result !== null && !result.session.user && !result.error;
72
+ const isLoading = result === null || silentPending;
40
73
  const session = result?.session ?? null;
41
74
  const error = result?.error ?? null;
42
75
  // One entry per path. Held in a ref, not state, because a cache write must
@@ -116,6 +149,21 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
116
149
  // eslint-disable-next-line react-hooks/set-state-in-effect
117
150
  void refresh();
118
151
  }, [refresh]);
152
+ (0, react_1.useEffect)(() => {
153
+ // `silentPending` already encodes settled-and-signed-out, no prior
154
+ // attempt, and no bridge error - a failed bridge read says nothing about
155
+ // whether Connect has a session, and bouncing the browser on a transient
156
+ // network failure would turn a recoverable error into a navigation nobody
157
+ // asked for.
158
+ if (!silentPending || typeof window === "undefined")
159
+ return;
160
+ // Written before navigating, not after: a navigation that begins before
161
+ // the marker lands leaves the tab unmarked and free to probe again on the
162
+ // way back, which is the loop this bounds.
163
+ window.sessionStorage.setItem(SILENT_ATTEMPT_KEY, "1");
164
+ const next = `${window.location.pathname}${window.location.search}`;
165
+ window.location.href = `${basePath}/signin?prompt=none&next=${encodeURIComponent(next)}`;
166
+ }, [silentPending, basePath]);
119
167
  const signIn = (0, react_1.useCallback)((options = {}) => {
120
168
  const url = new URL("/sso/authorize", appUrl);
121
169
  url.searchParams.set("client_id", clientId);
@@ -198,6 +246,10 @@ function useConnectPlans() {
198
246
  function useConnectApps() {
199
247
  return useConnectResource("/apps");
200
248
  }
249
+ /** Enabled Composio-hosted toolkits and this organization's connection status for each. */
250
+ function useConnectIntegrations() {
251
+ return useConnectResource("/integrations");
252
+ }
201
253
  /**
202
254
  * This Connect account's own login sessions - account-wide, not scoped to
203
255
  * this service, see `SessionsListResponse`'s doc comment. The component