@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.
- package/LICENSE +34 -0
- package/README.md +446 -0
- package/dist/cjs/cli/bin.js +16 -0
- package/dist/cjs/cli/init.js +179 -0
- package/dist/cjs/client/components/account-sessions.js +112 -0
- package/dist/cjs/client/components/apps-grid.js +46 -0
- package/dist/cjs/client/components/auth-buttons.js +42 -0
- package/dist/cjs/client/components/feature-gate.js +61 -0
- package/dist/cjs/client/components/link-button.js +24 -0
- package/dist/cjs/client/components/logo.js +27 -0
- package/dist/cjs/client/components/plan-pricing.js +166 -0
- package/dist/cjs/client/components/plans-page.js +39 -0
- package/dist/cjs/client/components/plans.js +212 -0
- package/dist/cjs/client/components/readiness.js +117 -0
- package/dist/cjs/client/components/subscribe-dialog.js +60 -0
- package/dist/cjs/client/components/subscription.js +189 -0
- package/dist/cjs/client/components/usage-meter.js +68 -0
- package/dist/cjs/client/components/user-menu.js +172 -0
- package/dist/cjs/client/index.js +99 -0
- package/dist/cjs/client/mark.js +16 -0
- package/dist/cjs/client/provider.js +259 -0
- package/dist/cjs/client/use-checkout.js +173 -0
- package/dist/cjs/errors.js +74 -0
- package/dist/cjs/format-relative-time.js +18 -0
- package/dist/cjs/helpers.js +30 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/next/index.js +311 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/client.js +114 -0
- package/dist/cjs/server/handler.js +333 -0
- package/dist/cjs/server/http.js +160 -0
- package/dist/cjs/server/index.js +18 -0
- package/dist/cjs/server/resources/access.js +28 -0
- package/dist/cjs/server/resources/ai.js +36 -0
- package/dist/cjs/server/resources/apps.js +30 -0
- package/dist/cjs/server/resources/billing.js +71 -0
- package/dist/cjs/server/resources/drive.js +39 -0
- package/dist/cjs/server/resources/entitlements.js +49 -0
- package/dist/cjs/server/resources/identity.js +194 -0
- package/dist/cjs/server/resources/logs.js +41 -0
- package/dist/cjs/server/resources/otp.js +29 -0
- package/dist/cjs/server/resources/sessions.js +82 -0
- package/dist/cjs/types.js +15 -0
- package/dist/esm/cli/bin.js +13 -0
- package/dist/esm/cli/init.js +174 -0
- package/dist/esm/client/components/account-sessions.js +109 -0
- package/dist/esm/client/components/apps-grid.js +43 -0
- package/dist/esm/client/components/auth-buttons.js +38 -0
- package/dist/esm/client/components/feature-gate.js +57 -0
- package/dist/esm/client/components/link-button.js +21 -0
- package/dist/esm/client/components/logo.js +24 -0
- package/dist/esm/client/components/plan-pricing.js +156 -0
- package/dist/esm/client/components/plans-page.js +36 -0
- package/dist/esm/client/components/plans.js +209 -0
- package/dist/esm/client/components/readiness.js +113 -0
- package/dist/esm/client/components/subscribe-dialog.js +57 -0
- package/dist/esm/client/components/subscription.js +181 -0
- package/dist/esm/client/components/usage-meter.js +65 -0
- package/dist/esm/client/components/user-menu.js +164 -0
- package/dist/esm/client/index.js +47 -0
- package/dist/esm/client/mark.js +13 -0
- package/dist/esm/client/provider.js +247 -0
- package/dist/esm/client/use-checkout.js +168 -0
- package/dist/esm/errors.js +69 -0
- package/dist/esm/format-relative-time.js +15 -0
- package/dist/esm/helpers.js +26 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/next/index.js +279 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/client.js +110 -0
- package/dist/esm/server/handler.js +330 -0
- package/dist/esm/server/http.js +156 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/server/resources/access.js +24 -0
- package/dist/esm/server/resources/ai.js +32 -0
- package/dist/esm/server/resources/apps.js +26 -0
- package/dist/esm/server/resources/billing.js +67 -0
- package/dist/esm/server/resources/drive.js +35 -0
- package/dist/esm/server/resources/entitlements.js +45 -0
- package/dist/esm/server/resources/identity.js +189 -0
- package/dist/esm/server/resources/logs.js +37 -0
- package/dist/esm/server/resources/otp.js +25 -0
- package/dist/esm/server/resources/sessions.js +78 -0
- package/dist/esm/types.js +14 -0
- package/dist/theme.css +84 -0
- package/dist/types/cli/bin.d.ts +1 -0
- package/dist/types/cli/init.d.ts +34 -0
- package/dist/types/client/components/account-sessions.d.ts +25 -0
- package/dist/types/client/components/apps-grid.d.ts +20 -0
- package/dist/types/client/components/auth-buttons.d.ts +31 -0
- package/dist/types/client/components/feature-gate.d.ts +57 -0
- package/dist/types/client/components/link-button.d.ts +20 -0
- package/dist/types/client/components/logo.d.ts +35 -0
- package/dist/types/client/components/plan-pricing.d.ts +96 -0
- package/dist/types/client/components/plans-page.d.ts +36 -0
- package/dist/types/client/components/plans.d.ts +34 -0
- package/dist/types/client/components/readiness.d.ts +52 -0
- package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
- package/dist/types/client/components/subscription.d.ts +123 -0
- package/dist/types/client/components/usage-meter.d.ts +24 -0
- package/dist/types/client/components/user-menu.d.ts +86 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/mark.d.ts +11 -0
- package/dist/types/client/provider.d.ts +209 -0
- package/dist/types/client/use-checkout.d.ts +93 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/format-relative-time.d.ts +2 -0
- package/dist/types/helpers.d.ts +33 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/next/index.d.ts +136 -0
- package/dist/types/server/client.d.ts +83 -0
- package/dist/types/server/handler.d.ts +67 -0
- package/dist/types/server/http.d.ts +52 -0
- package/dist/types/server/index.d.ts +15 -0
- package/dist/types/server/resources/access.d.ts +17 -0
- package/dist/types/server/resources/ai.d.ts +24 -0
- package/dist/types/server/resources/apps.d.ts +12 -0
- package/dist/types/server/resources/billing.d.ts +41 -0
- package/dist/types/server/resources/drive.d.ts +26 -0
- package/dist/types/server/resources/entitlements.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +76 -0
- package/dist/types/server/resources/logs.d.ts +20 -0
- package/dist/types/server/resources/otp.d.ts +17 -0
- package/dist/types/server/resources/sessions.d.ts +37 -0
- package/dist/types/types.d.ts +434 -0
- 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
|
+
}
|