@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,112 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectAccountSessions = ConnectAccountSessions;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("react");
7
+ const react_2 = require("@heroui/react");
8
+ const lucide_react_1 = require("lucide-react");
9
+ const provider_js_1 = require("../provider.js");
10
+ const format_relative_time_js_1 = require("../../format-relative-time.js");
11
+ const DEVICE_ICONS = {
12
+ desktop: lucide_react_1.Monitor,
13
+ mobile: lucide_react_1.Smartphone,
14
+ tablet: lucide_react_1.Tablet,
15
+ unknown: lucide_react_1.MonitorSmartphone,
16
+ };
17
+ function deviceLabel(session) {
18
+ const { device } = session;
19
+ if (device.browser)
20
+ return device.os ? `${device.browser} on ${device.os}` : device.browser;
21
+ if (device.os)
22
+ return device.os;
23
+ return device.deviceType === "unknown" ? "Unknown device" : device.deviceType;
24
+ }
25
+ function metaLine(session) {
26
+ const place = [session.ipCity, session.ipCountry].filter(Boolean).join(", ");
27
+ const active = session.isCurrent ? "active now" : `active ${(0, format_relative_time_js_1.formatRelativeTime)(new Date(session.lastActiveAt))}`;
28
+ return place ? `${place} · ${active}` : active;
29
+ }
30
+ /**
31
+ * Current device first, then most recently active. The row a user is most
32
+ * likely to *recognise* has to be the one they see without scrolling, or the
33
+ * list reads as a page of strangers and the whole surface loses its point -
34
+ * which is letting someone spot a sign-in that isn't theirs.
35
+ */
36
+ function ordered(sessions) {
37
+ return [...sessions].sort((a, b) => {
38
+ if (a.isCurrent !== b.isCurrent)
39
+ return a.isCurrent ? -1 : 1;
40
+ return new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime();
41
+ });
42
+ }
43
+ /**
44
+ * One row's revoke action, confirmed before it fires - revoking a session
45
+ * ends every partner-service sign-in from that same browser, not just this
46
+ * one. Closes on confirm, not on response - the same convention every
47
+ * destructive confirmation in this codebase follows - so the dialog itself
48
+ * needs no async/pending state of its own; the row simply disappears once
49
+ * `onRevoke`'s `refetch()` resolves.
50
+ *
51
+ * Icon-only, because the row already carries three pieces of text and a
52
+ * fourth ("Sign out", repeated per row) turns a scannable list into a wall.
53
+ * The accessible name still spells out *which* device, so a screen reader
54
+ * never hears three identically-named buttons - and that name is what the
55
+ * confirmation restates before anything is destroyed.
56
+ */
57
+ function RevokeRow({ session, onRevoke }) {
58
+ const label = deviceLabel(session);
59
+ return ((0, jsx_runtime_1.jsxs)(react_2.AlertDialog, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { isIconOnly: true, size: "sm", variant: "tertiary", className: "shrink-0 text-muted hover:text-danger", "aria-label": `Sign out ${label}`, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "size-4" }) }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Container, { children: (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Dialog, { className: "sm:max-w-100", children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.CloseTrigger, {}), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Header, { children: [(0, jsx_runtime_1.jsx)(react_2.AlertDialog.Icon, { status: "danger" }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Heading, { children: "Sign out this device?" })] }), (0, jsx_runtime_1.jsx)(react_2.AlertDialog.Body, { children: (0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-muted", children: ["This ends every sign-in from ", label, ", on every service, not just this one."] }) }), (0, jsx_runtime_1.jsxs)(react_2.AlertDialog.Footer, { children: [(0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "secondary", className: "text-xs", children: "Cancel" }), (0, jsx_runtime_1.jsx)(react_2.Button, { slot: "close", variant: "danger", className: "text-xs", onPress: onRevoke, children: "Sign out" })] })] }) }) })] }));
60
+ }
61
+ /**
62
+ * Every device signed into this Connect account - not scoped to this
63
+ * service, see `ConnectUserSession`'s doc comment. Owns the one
64
+ * `useConnectSessions()` call for this modal; every revoke action here
65
+ * mutates through it and re-reads the list itself, since nothing else
66
+ * invalidates it automatically - see `mutateResource`'s doc comment.
67
+ *
68
+ * That re-read is `invalidateResource("/sessions")` **then** `refetch()`,
69
+ * and both halves are load-bearing. `mutateResource` evicts only the path it
70
+ * posted to - `/sessions/revoke` - so `/sessions` keeps its cached promise,
71
+ * and `refetch()` alone just re-runs the effect and is handed that same
72
+ * resolved promise back. The row stayed on screen and the revoke looked like
73
+ * it had silently failed, when in fact the server had done exactly as asked.
74
+ */
75
+ function ConnectAccountSessions({ className, maxVisible = 4 }) {
76
+ const { mutateResource, invalidateResource } = (0, provider_js_1.useConnect)();
77
+ const { data, isLoading, error, refetch } = (0, provider_js_1.useConnectSessions)();
78
+ const [isRevokingOthers, setIsRevokingOthers] = (0, react_1.useState)(false);
79
+ const [showAll, setShowAll] = (0, react_1.useState)(false);
80
+ if (isLoading) {
81
+ return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-4 w-28" }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface", children: Array.from({ length: 2 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 px-3 py-3", children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "size-9 shrink-0 rounded-lg" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-3.5 w-32" }), (0, jsx_runtime_1.jsx)(react_2.Skeleton, { className: "h-3 w-24" })] })] }, index))) })] }));
82
+ }
83
+ // A failed read used to render as *nothing*, which is indistinguishable
84
+ // from "you have no other devices" - the one reading of this list that
85
+ // should never be guessed at.
86
+ if (error) {
87
+ return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-2 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-foreground", children: "Active sessions" }), (0, jsx_runtime_1.jsx)("p", { className: "rounded-xl border border-border bg-surface px-3 py-4 text-xs text-muted", children: "Could not load your sessions. Try again in a moment." })] }));
88
+ }
89
+ const sessions = ordered(data?.items ?? []);
90
+ if (sessions.length === 0)
91
+ return null;
92
+ const hasOtherSessions = sessions.some((session) => !session.isCurrent);
93
+ const isCollapsible = maxVisible > 0 && sessions.length > maxVisible;
94
+ const visible = isCollapsible && !showAll ? sessions.slice(0, maxVisible) : sessions;
95
+ return ((0, jsx_runtime_1.jsxs)("section", { className: `flex flex-col gap-3 ${className ?? ""}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [(0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: ["Active sessions", (0, jsx_runtime_1.jsx)(react_2.Chip, { size: "sm", variant: "soft", children: sessions.length })] }), (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-muted", children: "Devices signed in to your Connect account." })] }), hasOtherSessions && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "shrink-0 text-xs text-danger", isPending: isRevokingOthers, isDisabled: isRevokingOthers, onPress: () => {
96
+ setIsRevokingOthers(true);
97
+ void mutateResource("/sessions/revoke-others")
98
+ .then(() => {
99
+ invalidateResource("/sessions");
100
+ refetch();
101
+ })
102
+ .finally(() => setIsRevokingOthers(false));
103
+ }, children: "Sign out others" }))] }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface", children: visible.map((session) => {
104
+ const Icon = DEVICE_ICONS[session.device.deviceType];
105
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 px-3 py-2.5", children: [(0, jsx_runtime_1.jsx)("span", { className: `flex size-9 shrink-0 items-center justify-center rounded-lg ${session.isCurrent ? "bg-accent-soft text-accent-soft-foreground" : "bg-surface-secondary text-muted"}`, children: (0, jsx_runtime_1.jsx)(Icon, { className: "size-4", "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 flex-1", children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { className: "truncate text-sm font-medium text-foreground", children: deviceLabel(session) }), session.isCurrent && ((0, jsx_runtime_1.jsx)(react_2.Chip, { color: "accent", size: "sm", variant: "soft", className: "shrink-0", children: "This device" }))] }), (0, jsx_runtime_1.jsx)("p", { className: "truncate text-xs text-muted", children: metaLine(session) })] }), !session.isCurrent && ((0, jsx_runtime_1.jsx)(RevokeRow, { session: session, onRevoke: () => {
106
+ void mutateResource("/sessions/revoke", { sessionId: session.id }).then(() => {
107
+ invalidateResource("/sessions");
108
+ refetch();
109
+ });
110
+ } }))] }, session.id));
111
+ }) }), isCollapsible && ((0, jsx_runtime_1.jsx)(react_2.Button, { size: "sm", variant: "tertiary", className: "self-start text-xs", onPress: () => setShowAll((open) => !open), children: showAll ? "Show fewer" : `Show all ${sessions.length} devices` }))] }));
112
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectAppsGrid = ConnectAppsGrid;
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
+ function AppTile({ app }) {
10
+ const disabled = app.launchUrl === null;
11
+ const body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("span", { className: "relative flex size-12 items-center justify-center overflow-hidden rounded-xl bg-surface-secondary", children: [app.iconUrl ? (
12
+ // Deliberately a plain `<img>`: this package ships into any React app
13
+ // and cannot assume `next/image` or any other framework loader.
14
+ // eslint-disable-next-line @next/next/no-img-element
15
+ (0, jsx_runtime_1.jsx)("img", { src: app.iconUrl, alt: "", className: "size-full object-cover" })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-6 text-muted", "aria-hidden": "true" })), app.needsAttention && ((0, jsx_runtime_1.jsx)("span", { className: "absolute -right-0.5 -top-0.5 flex size-4 items-center justify-center rounded-full bg-warning text-warning-foreground", children: (0, jsx_runtime_1.jsx)(lucide_react_1.AlertCircle, { className: "size-3", "aria-hidden": "true" }) }))] }), (0, jsx_runtime_1.jsx)("span", { className: "line-clamp-2 text-center text-xs font-medium text-foreground", children: app.name }), app.isCurrent && (0, jsx_runtime_1.jsx)("span", { className: "text-[0.625rem] uppercase tracking-wide text-accent", children: "Current" })] }));
16
+ const shared = "flex flex-col items-center gap-2 rounded-xl p-3 transition-colors focus-visible:outline-2 focus-visible:outline-focus";
17
+ if (disabled) {
18
+ return ((0, jsx_runtime_1.jsx)("div", { className: `${shared} cursor-default opacity-60`, title: `${app.name} has no launch URL configured`, children: body }));
19
+ }
20
+ return ((0, jsx_runtime_1.jsx)("a", { href: app.launchUrl, target: "_blank", rel: "noopener noreferrer", className: `${shared} hover:bg-surface-secondary`, title: app.description || app.name, children: body }));
21
+ }
22
+ /**
23
+ * The Google-style app launcher: a grid icon in your top bar that opens a
24
+ * modal of every Connect service this organization can actually open.
25
+ *
26
+ * Only launchable apps appear, so no tile is a dead end - a paused
27
+ * subscription still shows, badged, because a dunning cycle should not make a
28
+ * product look deleted. The current app is pinned first and marked, rather
29
+ * than filtered out, so the grid never has a hole where "you are here" should
30
+ * be.
31
+ *
32
+ * Renders nothing when signed out: a launcher with nothing to launch is just a
33
+ * button that opens an empty box.
34
+ */
35
+ function ConnectAppsGrid({ className, label = "Switch app" }) {
36
+ const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
37
+ const { data, isLoading, error } = (0, provider_js_1.useConnectApps)();
38
+ if (sessionLoading || !session?.user)
39
+ return null;
40
+ const apps = data?.apps ?? [];
41
+ return ((0, jsx_runtime_1.jsxs)(react_1.Modal, { children: [(0, jsx_runtime_1.jsx)(react_1.Button, { isIconOnly: true, variant: "primary", "aria-label": label, className: className, children: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { className: "size-5" }) }), (0, jsx_runtime_1.jsx)(react_1.Modal.Backdrop, { children: (0, jsx_runtime_1.jsx)(react_1.Modal.Container, { children: (0, jsx_runtime_1.jsxs)(react_1.Modal.Dialog, { className: "sm:max-w-md", 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: "Your apps" }) }), (0, jsx_runtime_1.jsxs)(react_1.Modal.Body, { children: [isLoading && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: Array.from({ length: 6 }, (_, index) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center gap-2 p-3", children: [(0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "size-12 rounded-xl" }), (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: "h-3 w-14" })] }, index))) })), !isLoading && error && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-danger", children: "Could not load your apps." })), !isLoading && !error && apps.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: "py-6 text-center text-sm text-muted", children: "No other apps yet. Anything this workspace subscribes to shows up here." })), !isLoading && !error && apps.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-3 gap-2", children: apps.map((app) => ((0, jsx_runtime_1.jsx)(AppTile, { app: app }, app.serviceId))) }))] }), (0, jsx_runtime_1.jsx)(react_1.Modal.Footer, { children: (0, jsx_runtime_1.jsx)(ConnectBrowseAllLink, {}) })] }) }) })] }));
42
+ }
43
+ function ConnectBrowseAllLink() {
44
+ const { appUrl } = (0, provider_js_1.useConnect)();
45
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: "w-full", onPress: () => window.open(`${appUrl}/services`, "_blank", "noopener,noreferrer"), children: ["Browse all services", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 text-muted" })] }));
46
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectLoginButton = ConnectLoginButton;
5
+ exports.ConnectRegisterButton = ConnectRegisterButton;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("@heroui/react");
8
+ const provider_js_1 = require("../provider.js");
9
+ const mark_js_1 = require("../mark.js");
10
+ /**
11
+ * "Continue with Connect" - the federated sign-in button, in the shape people
12
+ * already know from Google and GitHub: neutral surface, brand mark on the
13
+ * left, one line of text.
14
+ *
15
+ * `variant="secondary"` by default rather than `primary`, on purpose. A
16
+ * federated button that outshouts the host app's own primary action reads as
17
+ * an ad; every mature identity button is quiet. Pass `variant` to override if
18
+ * Connect *is* the primary path in your flow.
19
+ *
20
+ * Hidden while a session is still loading, so a signed-in user never sees a
21
+ * Login button flash before their avatar resolves.
22
+ */
23
+ function ConnectLoginButton({ children, ...props }) {
24
+ const { signIn, isLoading, session } = (0, provider_js_1.useConnect)();
25
+ if (isLoading || session?.user)
26
+ return null;
27
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => signIn(), ...props, children: [(0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, {}), children ?? "Continue with Connect"] }));
28
+ }
29
+ /**
30
+ * The same button pointed at registration (`prompt=create`).
31
+ *
32
+ * Only the starting screen differs - each screen links to the other carrying
33
+ * the same in-flight request, and anyone who already has a Connect session
34
+ * skips straight to consent either way. So shipping both buttons costs nothing
35
+ * and matches what users expect from a "Sign up" / "Log in" pair.
36
+ */
37
+ function ConnectRegisterButton({ children, ...props }) {
38
+ const { signIn, isLoading, session } = (0, provider_js_1.useConnect)();
39
+ if (isLoading || session?.user)
40
+ return null;
41
+ return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "primary", onPress: () => signIn({ prompt: "create" }), ...props, children: [(0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, {}), children ?? "Sign up with Connect"] }));
42
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.useConnectFeature = useConnectFeature;
5
+ exports.ConnectFeatureGate = ConnectFeatureGate;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const provider_js_1 = require("../provider.js");
8
+ const subscription_js_1 = require("./subscription.js");
9
+ /**
10
+ * Whether the live subscription unlocks a plan feature.
11
+ *
12
+ * The browser twin of `client.entitlements.hasFeature` on the server, and the
13
+ * same posture: `false` rather than a throw when there is no subscription at
14
+ * all, because "not entitled" is the same answer either way and a feature check
15
+ * should not need a try/catch around it.
16
+ *
17
+ * `isLoading` is returned separately and matters: `false` while the first read
18
+ * is in flight means "we don't know yet", not "no". Gate on `isEntitled` for
19
+ * what to render and on `isLoading` for whether to render anything at all -
20
+ * `ConnectFeatureGate` does exactly that.
21
+ */
22
+ function useConnectFeature(feature) {
23
+ const { data, isLoading } = (0, provider_js_1.useConnectEntitlements)();
24
+ const features = data?.subscription.plan?.features ?? [];
25
+ return {
26
+ // `hasActiveSubscription` as well as the key: a cancelled or paused
27
+ // subscription still carries its plan's feature list, and reading that
28
+ // alone would keep unlocking a module nobody is paying for.
29
+ isEntitled: (data?.subscription.hasActiveSubscription ?? false) && features.includes(feature),
30
+ isLoading,
31
+ features,
32
+ };
33
+ }
34
+ /**
35
+ * Renders `children` only when the workspace's plan includes `feature`.
36
+ *
37
+ * ```tsx
38
+ * <ConnectFeatureGate feature="advanced_reports" title="Reports are on Growth">
39
+ * <Reports />
40
+ * </ConnectFeatureGate>
41
+ * ```
42
+ *
43
+ * Like every gate in this package, it is a **UX gate, not a security
44
+ * boundary**: the entitlement is read through your own origin and the browser
45
+ * can ignore it. Check `client.entitlements.hasFeature(orgId, feature)` on the
46
+ * server before doing the thing the feature unlocks.
47
+ *
48
+ * Renders `loading` - not `fallback` - while the answer is unknown, so a paying
49
+ * customer never sees an upsell for something they already have.
50
+ */
51
+ function ConnectFeatureGate({ feature, children, fallback, loading = null, plansHref, title, description, className, }) {
52
+ const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
53
+ const { isEntitled, isLoading } = useConnectFeature(feature);
54
+ if (sessionLoading || (session?.user && isLoading))
55
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: loading });
56
+ if (isEntitled)
57
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: children });
58
+ if (fallback !== undefined)
59
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback });
60
+ return ((0, jsx_runtime_1.jsx)(subscription_js_1.ConnectNoSubscriptionCard, { plansHref: plansHref, title: title ?? "Not included in your plan", description: description ?? "This part of the service is on a higher plan. Compare plans to see what unlocks it.", className: className }));
61
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectLinkButton = ConnectLinkButton;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const react_1 = require("@heroui/react");
7
+ /**
8
+ * A HeroUI `Button` that renders a real `<a>`.
9
+ *
10
+ * A plain anchor, deliberately not a router link: this package imports nothing
11
+ * from `next` (see the `./next` entry's doc comment for why that is a hard
12
+ * rule), so it cannot know which router - if any - the host app uses. An
13
+ * `<a href>` navigates under all of them, and a host that wants client-side
14
+ * routing can pass its own element via `render` on the underlying `Button`.
15
+ *
16
+ * The cast is the same one HeroUI's own "Render Function" example needs:
17
+ * `render` types its argument as the props of the element `Button` would have
18
+ * emitted, which is a `<button>` - including a `ref` typed to
19
+ * `HTMLButtonElement`. Spreading that onto an anchor is sound at runtime and
20
+ * unrepresentable in the type, which is exactly what a cast is for.
21
+ */
22
+ function ConnectLinkButton({ href, ...props }) {
23
+ return ((0, jsx_runtime_1.jsx)(react_1.Button, { ...props, render: (renderProps) => (0, jsx_runtime_1.jsx)("a", { ...renderProps, href: href }) }));
24
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectLogo = ConnectLogo;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const mark_js_1 = require("../mark.js");
7
+ /**
8
+ * The shared platform lockup: a service's own glyph on the CosmoOps badge,
9
+ * `cosmo·ops` beside it, and the service name underneath.
10
+ *
11
+ * This is what makes a family of service platforms look like one product. Each
12
+ * platform passes only what differs - its `name` and its `icon` - and inherits
13
+ * the mark geometry, the wordmark treatment, the separator, and the accent from
14
+ * one place. Change the lockup here and every platform follows.
15
+ *
16
+ * Sizes are `em`-based throughout, so the whole lockup scales from the
17
+ * wrapper's font size: `className="text-base"` renders it half again as large
18
+ * as `text-sm`, with the badge, gap and tracking all in proportion. That is
19
+ * why there is no `size` prop.
20
+ */
21
+ function ConnectLogo({ name, icon, collapsed = false, className }) {
22
+ const badge = ((0, jsx_runtime_1.jsxs)("span", { className: "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-[0.28em] bg-accent text-accent-foreground", style: { width: "2em", height: "2em" }, "aria-hidden": "true", children: [icon ? (0, jsx_runtime_1.jsx)("span", { className: "inline-flex items-center justify-center [&>*]:size-[1.25em]", children: icon }) : null, !icon && (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: "absolute inset-0 size-full" })] }));
23
+ if (collapsed) {
24
+ return ((0, jsx_runtime_1.jsx)("span", { className: `inline-flex text-sm leading-none ${className ?? ""}`, role: "img", "aria-label": name, children: badge }));
25
+ }
26
+ return ((0, jsx_runtime_1.jsxs)("span", { className: `inline-flex min-w-0 items-center gap-[0.6em] text-sm leading-none ${className ?? ""}`, role: "img", "aria-label": `cosmoops ${name}`, children: [badge, (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col leading-none", children: [(0, jsx_runtime_1.jsxs)("span", { className: "inline-flex items-center font-extrabold tracking-tight text-foreground", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("span", { children: "cosmo" }), (0, jsx_runtime_1.jsx)("span", { className: "mx-[0.12em] inline-block size-[0.19em] shrink-0 rounded-[0.04em] bg-accent" }), (0, jsx_runtime_1.jsx)("span", { children: "ops" })] }), (0, jsx_runtime_1.jsx)("span", { className: "mt-[0.42em] truncate text-[0.6em] font-semibold uppercase tracking-[0.26em] text-accent", "aria-hidden": "true", children: name })] })] }));
27
+ }
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * The pricing arithmetic the plan grid and its confirmation dialog have to
4
+ * agree on. Extracted so they cannot drift: a dialog that called an upgrade
5
+ * an upgrade while the card behind it said downgrade would be worse than
6
+ * either label being wrong on its own.
7
+ *
8
+ * Mirrors `packages/data/src/services/catalog.ts` and
9
+ * `packages/data/src/payments/plan-change.ts`, which are the copies Connect's
10
+ * own UI uses and which decide which use-case actually runs. The duplication
11
+ * is deliberate and load-bearing: this package carries no dependency on
12
+ * `@connect/data` so a partner's bundle never pulls Connect's internals, and
13
+ * `conformance.test.ts` type-checks the wire shapes against the server DTOs so
14
+ * the two can't drift silently.
15
+ *
16
+ * No React here - it is imported by both a client component and its dialog,
17
+ * and nothing in it needs to be.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.ACTION_LABEL = void 0;
21
+ exports.formatPrice = formatPrice;
22
+ exports.monthlyOf = monthlyOf;
23
+ exports.discountLabel = discountLabel;
24
+ exports.resolvePlanGroup = resolvePlanGroup;
25
+ exports.groupPlansByTier = groupPlansByTier;
26
+ exports.annualSaving = annualSaving;
27
+ exports.resolveAction = resolveAction;
28
+ /** Paise are the wire unit everywhere in Connect; rupees are the display unit. */
29
+ function formatPrice(paise) {
30
+ return new Intl.NumberFormat("en-IN", {
31
+ style: "currency",
32
+ currency: "INR",
33
+ maximumFractionDigits: paise % 100 === 0 ? 0 : 2,
34
+ }).format(paise / 100);
35
+ }
36
+ /**
37
+ * An annual plan's price is 12 cycles of the same product, so any comparison
38
+ * across billing intervals has to normalise first - otherwise every annual
39
+ * plan reads as an upgrade over every monthly one.
40
+ */
41
+ function monthlyOf(price, billingInterval) {
42
+ return billingInterval === "annual" ? price / 12 : price;
43
+ }
44
+ function discountLabel(discount) {
45
+ const amount = discount.discountType === "percentage" ? `${discount.value}% off` : `${formatPrice(discount.value)} off`;
46
+ if (discount.duration === "forever")
47
+ return `${amount}, forever`;
48
+ if (discount.cyclesRemaining === null)
49
+ return amount;
50
+ return `${amount}, ${discount.cyclesRemaining} ${discount.cyclesRemaining === 1 ? "cycle" : "cycles"} left`;
51
+ }
52
+ /**
53
+ * Falls back to a slug of the name for a Connect deployment predating
54
+ * `planGroup` on the wire, mirroring `resolvePlanGroup` server-side. Kept so
55
+ * an SDK upgrade doesn't require the partner's Connect instance to be
56
+ * upgraded in lockstep - an older server simply omits the field and the grid
57
+ * groups the way it always did.
58
+ */
59
+ function resolvePlanGroup(plan) {
60
+ const stored = plan.planGroup?.trim();
61
+ if (stored)
62
+ return stored;
63
+ const tokens = plan.name
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, "-")
66
+ .split("-")
67
+ .filter(Boolean);
68
+ const cadence = new Set(["monthly", "month", "annual", "annually", "yearly", "year", "mo", "yr"]);
69
+ const kept = tokens.filter((token, index) => {
70
+ if (cadence.has(token))
71
+ return false;
72
+ return !(token === "per" && cadence.has(tokens[index + 1] ?? ""));
73
+ });
74
+ return (kept.length > 0 ? kept : tokens).join("-") || "plan";
75
+ }
76
+ /**
77
+ * Collapses a flat plan list into one card per tier.
78
+ *
79
+ * This is the shape the grid wants and the old one didn't have. It used to
80
+ * filter the whole list down to the toggled interval, which meant a tier sold
81
+ * monthly-only simply vanished when a customer looked at annual pricing, and a
82
+ * catalog with no monthly plans at all rendered an empty grid on first paint.
83
+ * Grouping first means the toggle picks a *price inside a card*, and a card
84
+ * with only one cadence still shows.
85
+ *
86
+ * Ordered by the tier's cheapest monthly-equivalent price, so the grid reads
87
+ * left-to-right cheapest-first regardless of what order the API returned.
88
+ */
89
+ function groupPlansByTier(plans) {
90
+ const tiers = new Map();
91
+ for (const plan of plans) {
92
+ const planGroup = resolvePlanGroup(plan);
93
+ const tier = tiers.get(planGroup) ?? {
94
+ planGroup,
95
+ name: plan.name,
96
+ description: plan.description,
97
+ monthly: null,
98
+ annual: null,
99
+ annualSavingPercent: null,
100
+ };
101
+ if (plan.billingInterval === "monthly") {
102
+ if (!tier.monthly || plan.price < tier.monthly.price)
103
+ tier.monthly = plan;
104
+ }
105
+ else if (!tier.annual || plan.price < tier.annual.price) {
106
+ tier.annual = plan;
107
+ }
108
+ tiers.set(planGroup, tier);
109
+ }
110
+ return [...tiers.values()]
111
+ .map((tier) => {
112
+ const identity = tier.monthly ?? tier.annual;
113
+ return {
114
+ ...tier,
115
+ name: identity?.name ?? tier.name,
116
+ description: identity?.description ?? tier.description,
117
+ annualSavingPercent: annualSaving(tier.monthly, tier.annual),
118
+ };
119
+ })
120
+ .sort((a, b) => tierFloorPrice(a) - tierFloorPrice(b));
121
+ }
122
+ function tierFloorPrice(tier) {
123
+ const prices = [tier.monthly, tier.annual]
124
+ .filter((plan) => plan !== null)
125
+ .map((plan) => monthlyOf(plan.price, plan.billingInterval));
126
+ return prices.length > 0 ? Math.min(...prices) : Number.MAX_SAFE_INTEGER;
127
+ }
128
+ /**
129
+ * Annual is quoted per year on the wire (`billingInterval: "annual"`), so a
130
+ * side-by-side grid has to normalise before it can compare. Showing the saving
131
+ * against 12x the monthly plan is the comparison a customer is actually making.
132
+ *
133
+ * **Call this with a matched pair from one tier.** It used to be handed the
134
+ * first monthly and the first annual plan in the whole response - two plans
135
+ * that were the same tier only by the accident of document order, and that
136
+ * produced one badge for a catalog where each tier discounts differently.
137
+ * `groupPlansByTier` now does the pairing.
138
+ */
139
+ function annualSaving(monthly, annual) {
140
+ if (!monthly || !annual)
141
+ return null;
142
+ const saving = monthly.price * 12 - annual.price;
143
+ return saving > 0 ? Math.round((saving / (monthly.price * 12)) * 100) : null;
144
+ }
145
+ function resolveAction(plan, current) {
146
+ if (!current?.subscription || !current.plan)
147
+ return "subscribe";
148
+ if (current.plan.id === plan.id)
149
+ return "current";
150
+ const currentPlan = current.plan;
151
+ if (currentPlan.billingInterval !== plan.billingInterval) {
152
+ return plan.billingInterval === "annual" ? "switch-to-annual" : "switch-to-monthly";
153
+ }
154
+ return monthlyOf(plan.price, plan.billingInterval) >
155
+ monthlyOf(currentPlan.price, currentPlan.billingInterval)
156
+ ? "upgrade"
157
+ : "downgrade";
158
+ }
159
+ exports.ACTION_LABEL = {
160
+ current: "Current plan",
161
+ upgrade: "Upgrade",
162
+ downgrade: "Downgrade",
163
+ "switch-to-annual": "Switch to annual",
164
+ "switch-to-monthly": "Switch to monthly",
165
+ subscribe: "Subscribe",
166
+ };
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ConnectPlansPage = ConnectPlansPage;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const plans_js_1 = require("./plans.js");
7
+ const readiness_js_1 = require("./readiness.js");
8
+ const subscription_js_1 = require("./subscription.js");
9
+ const provider_js_1 = require("../provider.js");
10
+ const auth_buttons_js_1 = require("./auth-buttons.js");
11
+ /**
12
+ * A complete `/plans` page, assembled from the pieces so the host app's route
13
+ * file is one line:
14
+ *
15
+ * ```tsx
16
+ * // app/plans/page.tsx
17
+ * "use client";
18
+ * export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
19
+ * ```
20
+ *
21
+ * The assembly is the value here, not the layout. Three things have to be on
22
+ * this page and are easy to leave off:
23
+ *
24
+ * 1. **The readiness banner, above the grid.** A workspace that still needs
25
+ * KYC can press Subscribe all day and be refused at the last step. Saying so
26
+ * before they pick is the difference between a page that works and one that
27
+ * ends in a rejected charge.
28
+ * 2. **What they're on now**, so "Upgrade" on a card has something to mean.
29
+ * 3. **A signed-out state that is a login button**, not an empty grid - the
30
+ * catalogue reads through the bridge, which needs a session.
31
+ *
32
+ * Every prop `ConnectPlans` takes passes straight through, and each piece is
33
+ * exported on its own for an app that wants a different arrangement.
34
+ */
35
+ function ConnectPlansPage({ title = "Plans", description = "Pick the plan that fits. Change or cancel it whenever you like.", showCurrentSubscription = true, ...plansProps }) {
36
+ const { session, isLoading } = (0, provider_js_1.useConnect)();
37
+ const signedOut = !isLoading && !session?.user;
38
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-8", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-semibold text-foreground", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: description })] }), signedOut ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-muted", children: "Sign in to see your plan options and what you're on today." }), (0, jsx_runtime_1.jsx)(auth_buttons_js_1.ConnectLoginButton, {})] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(readiness_js_1.ConnectReadinessBanner, {}), showCurrentSubscription && (0, jsx_runtime_1.jsx)(subscription_js_1.ConnectSubscriptionCard, {}), (0, jsx_runtime_1.jsx)(plans_js_1.ConnectPlans, { ...plansProps })] }))] }));
39
+ }