@cosmoops/connect-sdk 0.41.0 → 0.43.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/CHANGELOG.md +99 -0
- package/dist/cjs/client/components/access-gate.js +18 -40
- package/dist/cjs/client/components/apps-grid.js +11 -3
- package/dist/cjs/client/components/ask-ai-errors.js +109 -0
- package/dist/cjs/client/components/ask-ai-turns.js +12 -0
- package/dist/cjs/client/components/ask-ai.js +75 -8
- package/dist/cjs/client/components/plans.js +8 -2
- package/dist/cjs/client/components/readiness.js +2 -1
- package/dist/cjs/client/components/subscription.js +4 -4
- package/dist/cjs/client/components/user-menu.js +16 -11
- package/dist/cjs/client/handoff.js +78 -0
- package/dist/cjs/client/index.js +9 -6
- package/dist/cjs/client/provider.js +75 -4
- package/dist/cjs/client/use-checkout.js +23 -16
- package/dist/cjs/server/client.js +5 -1
- package/dist/cjs/server/handler.js +4 -1
- package/dist/cjs/server/resources/calls.js +5 -2
- package/dist/cjs/server/resources/identity.js +12 -0
- package/dist/cjs/server/resources/whatsapp.js +58 -0
- package/dist/esm/client/components/access-gate.js +18 -40
- package/dist/esm/client/components/apps-grid.js +11 -3
- package/dist/esm/client/components/ask-ai-errors.js +102 -0
- package/dist/esm/client/components/ask-ai-turns.js +12 -0
- package/dist/esm/client/components/ask-ai.js +76 -9
- package/dist/esm/client/components/plans.js +8 -2
- package/dist/esm/client/components/readiness.js +2 -1
- package/dist/esm/client/components/subscription.js +4 -4
- package/dist/esm/client/components/user-menu.js +16 -11
- package/dist/esm/client/handoff.js +72 -0
- package/dist/esm/client/index.js +4 -3
- package/dist/esm/client/provider.js +75 -4
- package/dist/esm/client/use-checkout.js +23 -16
- package/dist/esm/server/client.js +5 -1
- package/dist/esm/server/handler.js +4 -1
- package/dist/esm/server/resources/calls.js +5 -2
- package/dist/esm/server/resources/identity.js +12 -0
- package/dist/esm/server/resources/whatsapp.js +54 -0
- package/dist/types/client/components/ask-ai-errors.d.ts +41 -0
- package/dist/types/client/components/ask-ai-turns.d.ts +5 -0
- package/dist/types/client/handoff.d.ts +62 -0
- package/dist/types/client/index.d.ts +3 -1
- package/dist/types/client/provider.d.ts +72 -5
- package/dist/types/client/use-checkout.d.ts +4 -1
- package/dist/types/server/client.d.ts +4 -1
- package/dist/types/server/resources/identity.d.ts +10 -0
- package/dist/types/server/resources/whatsapp.d.ts +44 -0
- package/dist/types/types.d.ts +102 -2
- package/package.json +1 -1
|
@@ -80,13 +80,13 @@ function ConnectNoSubscriptionCard({ plansHref = DEFAULT_PLANS_HREF, title = "Yo
|
|
|
80
80
|
* impure and re-renders wrong.
|
|
81
81
|
*/
|
|
82
82
|
function ConnectSubscriptionCard({ plansHref, className }) {
|
|
83
|
-
const {
|
|
83
|
+
const { openConnect } = (0, provider_js_1.useConnect)();
|
|
84
84
|
const { isLoading, detail, plan, state } = useSubscriptionState();
|
|
85
85
|
if (isLoading)
|
|
86
86
|
return (0, jsx_runtime_1.jsx)(react_1.Skeleton, { className: (0, cn_js_1.cn)("h-48 rounded-xl", className) });
|
|
87
87
|
if (!detail || !plan)
|
|
88
88
|
return (0, jsx_runtime_1.jsx)(ConnectNoSubscriptionCard, { plansHref: plansHref, className: className });
|
|
89
|
-
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", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (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: () =>
|
|
89
|
+
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", " ", (0, plan_pricing_js_1.intervalLabel)(plan.billingInterval).per] })] }), (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: () => openConnect(`/billing/subscriptions/${encodeURIComponent(detail.id)}`), 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" })] })] }) }) }));
|
|
90
90
|
}
|
|
91
91
|
function SubscriptionChip({ state }) {
|
|
92
92
|
switch (state) {
|
|
@@ -157,11 +157,11 @@ function ConnectSeatUsage({ plan, detail, className, }) {
|
|
|
157
157
|
* actually cares about anyway.
|
|
158
158
|
*/
|
|
159
159
|
function ConnectTrialBanner({ plansHref, className }) {
|
|
160
|
-
const {
|
|
160
|
+
const { openConnect } = (0, provider_js_1.useConnect)();
|
|
161
161
|
const { detail, state } = useSubscriptionState();
|
|
162
162
|
if (state !== "trialing" || !detail)
|
|
163
163
|
return null;
|
|
164
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () =>
|
|
164
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_js_1.cn)("flex flex-wrap items-center justify-between gap-3 rounded-lg bg-accent-soft px-3 py-2 text-sm text-accent", className), children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: "size-4 shrink-0", "aria-hidden": "true" }), "You're on a free trial", detail.nextBillingDate ? `. Your first charge is on ${formatDate(detail.nextBillingDate)}.` : "."] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(link_button_js_1.ConnectLinkButton, { href: plansHref ?? DEFAULT_PLANS_HREF, variant: "tertiary", children: "Compare plans" }), (0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", onPress: () => openConnect(`/billing/subscriptions/${encodeURIComponent(detail.id)}`), children: ["Manage", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] })] })] }));
|
|
165
165
|
}
|
|
166
166
|
/**
|
|
167
167
|
* Renders `children` for a subscribed workspace, and the "pick a plan" card for
|
|
@@ -13,7 +13,6 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
13
13
|
const react_1 = require("@heroui/react");
|
|
14
14
|
const lucide_react_1 = require("lucide-react");
|
|
15
15
|
const provider_js_1 = require("../provider.js");
|
|
16
|
-
const portal_links_js_1 = require("../portal-links.js");
|
|
17
16
|
const mark_js_1 = require("../mark.js");
|
|
18
17
|
const identity_js_1 = require("./identity.js");
|
|
19
18
|
const account_sessions_js_1 = require("./account-sessions.js");
|
|
@@ -26,6 +25,12 @@ const THEME_OPTIONS = [
|
|
|
26
25
|
{ value: "dark", label: "Dark", icon: lucide_react_1.Moon },
|
|
27
26
|
{ value: "system", label: "System", icon: lucide_react_1.Laptop },
|
|
28
27
|
];
|
|
28
|
+
/**
|
|
29
|
+
* Connect's front page, which is a destination rather than a task - nothing to
|
|
30
|
+
* finish, so nothing to come back from. Every *task* destination here (Profile,
|
|
31
|
+
* Billing, Invoices) goes through `openConnect` instead, which brings the
|
|
32
|
+
* visitor back when they are done.
|
|
33
|
+
*/
|
|
29
34
|
function openExternal(url) {
|
|
30
35
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
31
36
|
}
|
|
@@ -100,8 +105,8 @@ function subscriptionChip(subscription) {
|
|
|
100
105
|
* 44%, and takes the tile down to ~88px while making its contents read
|
|
101
106
|
* bigger, not smaller.
|
|
102
107
|
*/
|
|
103
|
-
function DestinationTile({ icon, label,
|
|
104
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)
|
|
108
|
+
function DestinationTile({ icon, label, onPress, newTab, }) {
|
|
109
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", "aria-label": newTab ? `${label} (opens in a new tab)` : label, 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: onPress, children: [newTab && (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 })] }));
|
|
105
110
|
}
|
|
106
111
|
/**
|
|
107
112
|
* The account modal's contents - identity card, the three Connect
|
|
@@ -122,14 +127,14 @@ function DestinationTile({ icon, label, url, }) {
|
|
|
122
127
|
* three navigations.
|
|
123
128
|
*/
|
|
124
129
|
function ProfileSurface({ onSignOut, theme, onThemeChange, }) {
|
|
125
|
-
const { session, signOut, appUrl,
|
|
130
|
+
const { session, signOut, appUrl, openConnect, handoffMode } = (0, provider_js_1.useConnect)();
|
|
126
131
|
const user = session?.user;
|
|
127
132
|
if (!user)
|
|
128
133
|
return null;
|
|
129
134
|
const label = user.displayName ?? user.email ?? "Account";
|
|
130
135
|
const role = session?.role ?? null;
|
|
131
136
|
const planChip = subscriptionChip(session?.subscription ?? null);
|
|
132
|
-
return ((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.workspaceName }), (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",
|
|
137
|
+
return ((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.workspaceName }), (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", onPress: () => openConnect("/profile"), newTab: handoffMode === "popup" }), (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", onPress: () => openConnect("/billing"), newTab: handoffMode === "popup" }), (0, jsx_runtime_1.jsx)(DestinationTile, { icon: (0, jsx_runtime_1.jsx)(mark_js_1.ConnectMark, { className: surfaces_js_1.ICON }), label: "Connect", onPress: () => openExternal(appUrl), newTab: true })] }), 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) => {
|
|
133
138
|
const [next] = [...keys];
|
|
134
139
|
if (typeof next === "string")
|
|
135
140
|
onThemeChange(next);
|
|
@@ -207,8 +212,8 @@ function ConnectUserMenu({ compact = false, ...rest }) {
|
|
|
207
212
|
* state, a settings page.
|
|
208
213
|
*/
|
|
209
214
|
function ConnectManageSubscriptionButton({ className }) {
|
|
210
|
-
const {
|
|
211
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () =>
|
|
215
|
+
const { openConnect } = (0, provider_js_1.useConnect)();
|
|
216
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "secondary", className: className, onPress: () => openConnect("/billing"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.CreditCard, { className: "size-4 shrink-0" }), "Manage subscription", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
|
|
212
217
|
}
|
|
213
218
|
/**
|
|
214
219
|
* Invoices and payment history, on Connect.
|
|
@@ -221,10 +226,10 @@ function ConnectManageSubscriptionButton({ className }) {
|
|
|
221
226
|
* version of a record that must have exactly one.
|
|
222
227
|
*/
|
|
223
228
|
function ConnectInvoicesButton({ className }) {
|
|
224
|
-
const { session,
|
|
229
|
+
const { session, openConnect } = (0, provider_js_1.useConnect)();
|
|
225
230
|
if (!session?.user)
|
|
226
231
|
return null;
|
|
227
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () =>
|
|
232
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openConnect("/billing/invoices"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ReceiptText, { className: "size-4 shrink-0" }), "Invoices", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
|
|
228
233
|
}
|
|
229
234
|
/**
|
|
230
235
|
* The "Profile" action on its own - the third of the three Connect
|
|
@@ -236,10 +241,10 @@ function ConnectInvoicesButton({ className }) {
|
|
|
236
241
|
* and got no `return_to`, so the user had no way back.
|
|
237
242
|
*/
|
|
238
243
|
function ConnectProfileButton({ className }) {
|
|
239
|
-
const { session,
|
|
244
|
+
const { session, openConnect } = (0, provider_js_1.useConnect)();
|
|
240
245
|
if (!session?.user)
|
|
241
246
|
return null;
|
|
242
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () =>
|
|
247
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Button, { variant: "tertiary", className: className, onPress: () => openConnect("/profile"), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.UserRound, { className: "size-4 shrink-0" }), "Profile", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
|
|
243
248
|
}
|
|
244
249
|
/** A plain "Open Connect" button, for a footer or a help menu. */
|
|
245
250
|
function ConnectHomeButton({ className }) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The hand-off contract with Connect's portal - the pure half, so the rules
|
|
4
|
+
* can be tested without a browser. `openConnect` (on `useConnect()`) is the
|
|
5
|
+
* half that touches `window`.
|
|
6
|
+
*
|
|
7
|
+
* Every time this SDK sends somebody to Connect for a task - a plan, a
|
|
8
|
+
* payment method, their profile - it appends three params, and Connect's
|
|
9
|
+
* portal brings them back when the task is done:
|
|
10
|
+
*
|
|
11
|
+
* | Param | |
|
|
12
|
+
* |---|---|
|
|
13
|
+
* | `client_id` | Names this application. Connect only honours a `return_to` whose origin is registered for that client. |
|
|
14
|
+
* | `return_to` | The page they left. |
|
|
15
|
+
* | `mode` | `popup` - Connect closes its window when done and posts the outcome to this page first. `redirect` - Connect navigates back here with `?connect_result=`. |
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.CONNECT_HANDOFF_RESULT_PARAM = exports.CONNECT_HANDOFF_MESSAGE_TYPE = void 0;
|
|
19
|
+
exports.buildHandoffUrl = buildHandoffUrl;
|
|
20
|
+
exports.readHandoffMessage = readHandoffMessage;
|
|
21
|
+
exports.takeHandoffResult = takeHandoffResult;
|
|
22
|
+
/** `MessageEvent.data.type` of the message Connect posts before closing a popup. */
|
|
23
|
+
exports.CONNECT_HANDOFF_MESSAGE_TYPE = "connect:handoff";
|
|
24
|
+
/** The query param Connect adds when it redirects back. */
|
|
25
|
+
exports.CONNECT_HANDOFF_RESULT_PARAM = "connect_result";
|
|
26
|
+
const RESULTS = ["completed", "cancelled", "closed"];
|
|
27
|
+
function isResult(value) {
|
|
28
|
+
return typeof value === "string" && RESULTS.includes(value);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `target` - a path (`"/billing"`) or an absolute Connect URL (a readiness
|
|
32
|
+
* blocker's `resolveUrl`) - with the hand-off params set.
|
|
33
|
+
*
|
|
34
|
+
* Throws for a URL on any other origin: these params tell Connect where to
|
|
35
|
+
* send somebody back, and stamping them onto a third party's URL would hand
|
|
36
|
+
* that party the page this visitor was on.
|
|
37
|
+
*/
|
|
38
|
+
function buildHandoffUrl(target, { appUrl, clientId, mode, returnTo }) {
|
|
39
|
+
const url = new URL(target, appUrl);
|
|
40
|
+
if (url.origin !== new URL(appUrl).origin) {
|
|
41
|
+
throw new Error(`openConnect only opens Connect URLs (${new URL(appUrl).origin}), not ${url.origin}.`);
|
|
42
|
+
}
|
|
43
|
+
if (clientId)
|
|
44
|
+
url.searchParams.set("client_id", clientId);
|
|
45
|
+
url.searchParams.set("return_to", returnTo);
|
|
46
|
+
url.searchParams.set("mode", mode);
|
|
47
|
+
// `next` is the older spelling Connect still honours, and a stale one left
|
|
48
|
+
// on a server-built URL would compete with `return_to` - so it goes.
|
|
49
|
+
url.searchParams.delete("next");
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The result a `message` event carries, or `null` when it is not Connect's
|
|
54
|
+
* hand-off message. The origin check is the whole security story: any page
|
|
55
|
+
* can `postMessage` this window, and only Connect's verdict may trigger work.
|
|
56
|
+
*/
|
|
57
|
+
function readHandoffMessage(event, appUrl) {
|
|
58
|
+
if (event.origin !== new URL(appUrl).origin)
|
|
59
|
+
return null;
|
|
60
|
+
const data = event.data;
|
|
61
|
+
if (!data || typeof data !== "object")
|
|
62
|
+
return null;
|
|
63
|
+
const { type, result } = data;
|
|
64
|
+
return type === exports.CONNECT_HANDOFF_MESSAGE_TYPE && isResult(result) ? result : null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The result a redirect-mode return carries in `href`, and `href` with it
|
|
68
|
+
* removed - so a reload does not report the same trip twice and the param
|
|
69
|
+
* does not end up in a link somebody shares.
|
|
70
|
+
*/
|
|
71
|
+
function takeHandoffResult(href) {
|
|
72
|
+
const url = new URL(href);
|
|
73
|
+
const result = url.searchParams.get(exports.CONNECT_HANDOFF_RESULT_PARAM);
|
|
74
|
+
if (!isResult(result))
|
|
75
|
+
return null;
|
|
76
|
+
url.searchParams.delete(exports.CONNECT_HANDOFF_RESULT_PARAM);
|
|
77
|
+
return { result, cleaned: url.toString() };
|
|
78
|
+
}
|
package/dist/cjs/client/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
5
|
-
exports.
|
|
6
|
-
exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useUpdateEventType = exports.useCreateEventType = exports.useManageableEventTypes = exports.useUpdateAvailabilitySchedule = exports.useCreateAvailabilitySchedule = exports.useAvailabilitySchedules = exports.useBookableEventTypes = exports.ConnectEventTypeFormModal = exports.ConnectAvailabilityFormModal = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectCall = exports.ConnectCallingStatus = exports.ConnectCallButton = exports.ConnectCallProvider = exports.useCreateWorkspace = exports.ConnectCreateWorkspaceModal = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = void 0;
|
|
4
|
+
exports.RAIL_EXPANDED = exports.RAIL_COLLAPSED = exports.sidebarRowClass = exports.useConnectShell = exports.ConnectShellProvider = exports.ConnectSidebarRow = exports.ConnectSidebarFooter = exports.ConnectSidebarBody = exports.ConnectSidebarHeader = exports.ConnectSidebar = exports.AppShell = exports.useConnectPortalUrl = exports.connectPortalUrl = exports.ConnectAccountSessions = exports.ConnectSignOutButton = exports.ConnectHomeButton = exports.ConnectProfileButton = exports.ConnectInvoicesButton = exports.ConnectManageSubscriptionButton = exports.ConnectUserMenu = exports.ConnectProfileModal = exports.ConnectUserButton = exports.ConnectOneTap = exports.ConnectRegisterButton = exports.ConnectLoginButton = exports.ConnectLogo = exports.ConnectMark = exports.ConnectGrievanceNotice = exports.isConnectError = exports.ConnectError = exports.normalizePlanFeatures = exports.blockingBlockers = exports.CONNECT_HANDOFF_RESULT_PARAM = exports.CONNECT_HANDOFF_MESSAGE_TYPE = exports.CONNECT_APP_URL = exports.useConnectResource = exports.useConnectIntegrations = exports.useConnectSessions = exports.useConnectAddonUsage = exports.useConnectCompanyProfile = exports.useConnectApps = exports.useConnectPlans = exports.useConnectEntitlements = exports.useConnectPlatformAccess = exports.useConnectAudience = exports.useConnectWorkspaceType = exports.useConnectUser = exports.useConnectOptional = exports.useConnect = exports.ConnectProvider = void 0;
|
|
5
|
+
exports.ConnectPlansPage = exports.ConnectUsageMeter = exports.ConnectPlatformGate = exports.ConnectWorkspaceTypeGate = exports.useConnectFeature = 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.AI_TONE_DOT = exports.AI_TONE_CHIP = exports.AI_EMPTY_VALUE = exports.aiStatusTone = exports.isNumericAiFormat = exports.formatAiValue = exports.AiSources = exports.AiSteps = exports.AiDetails = exports.AiTimeline = exports.AiMetrics = exports.AiChart = exports.AiTable = exports.ConnectAskAi = exports.ConnectAppsGrid = exports.visitorSlotOfferFrom = exports.readVisitorStream = exports.ConnectVisitorChat = exports.isLinkableConnectScreen = exports.matchConnectScreen = exports.filterNavForAudience = exports.gateExemptPaths = exports.isNavItemActive = exports.resolveActivePath = exports.normalizePath = exports.flattenNav = exports.normalizeNav = exports.useConnectPush = exports.useActivePath = exports.ConnectSideNav = exports.ConnectSidebarNav = void 0;
|
|
6
|
+
exports.zonedInstant = exports.dayColumns = exports.layoutBookings = exports.useBookingActions = exports.useBookings = exports.useAvailableSlots = exports.useUpdateEventType = exports.useCreateEventType = exports.useManageableEventTypes = exports.useUpdateAvailabilitySchedule = exports.useCreateAvailabilitySchedule = exports.useAvailabilitySchedules = exports.useBookableEventTypes = exports.ConnectEventTypeFormModal = exports.ConnectAvailabilityFormModal = exports.ConnectBookingDetail = exports.ConnectBookingDialog = exports.ConnectBookingCalendar = exports.useConnectCall = exports.ConnectCallingStatus = exports.ConnectCallButton = exports.ConnectCallProvider = exports.useCreateWorkspace = exports.ConnectCreateWorkspaceModal = exports.useConnectWorkspaceMembers = exports.useConnectWorkspaces = exports.ConnectWorkspaceSwitcher = exports.resolvePlanGroup = exports.groupPlansByTier = exports.isFreePlan = exports.ACTION_LABEL = exports.discountLabel = exports.annualSaving = exports.checkoutSubscriptionId = exports.resolveAction = exports.monthlyOf = exports.formatPrice = exports.buildCheckoutUrl = exports.useConnectCheckout = exports.ConnectFilterTabs = exports.ConnectCursorPagination = exports.ConnectConfirmDialog = exports.ConnectLinkButton = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Browser half of `@cosmoops/connect-sdk` - React components and hooks.
|
|
9
9
|
*
|
|
@@ -35,6 +35,9 @@ Object.defineProperty(exports, "useConnectSessions", { enumerable: true, get: fu
|
|
|
35
35
|
Object.defineProperty(exports, "useConnectIntegrations", { enumerable: true, get: function () { return provider_js_1.useConnectIntegrations; } });
|
|
36
36
|
Object.defineProperty(exports, "useConnectResource", { enumerable: true, get: function () { return provider_js_1.useConnectResource; } });
|
|
37
37
|
Object.defineProperty(exports, "CONNECT_APP_URL", { enumerable: true, get: function () { return provider_js_1.CONNECT_APP_URL; } });
|
|
38
|
+
var handoff_js_1 = require("./handoff.js");
|
|
39
|
+
Object.defineProperty(exports, "CONNECT_HANDOFF_MESSAGE_TYPE", { enumerable: true, get: function () { return handoff_js_1.CONNECT_HANDOFF_MESSAGE_TYPE; } });
|
|
40
|
+
Object.defineProperty(exports, "CONNECT_HANDOFF_RESULT_PARAM", { enumerable: true, get: function () { return handoff_js_1.CONNECT_HANDOFF_RESULT_PARAM; } });
|
|
38
41
|
// Re-exported from the root entry rather than kept there alone: it is a pure
|
|
39
42
|
// helper, but every caller is UI code, and making a component file reach two
|
|
40
43
|
// entry points for one function is friction with nothing behind it.
|
|
@@ -70,9 +73,9 @@ Object.defineProperty(exports, "ConnectSignOutButton", { enumerable: true, get:
|
|
|
70
73
|
var account_sessions_js_1 = require("./components/account-sessions.js");
|
|
71
74
|
Object.defineProperty(exports, "ConnectAccountSessions", { enumerable: true, get: function () { return account_sessions_js_1.ConnectAccountSessions; } });
|
|
72
75
|
// A link to *any* Connect portal page that offers a `"Back to <your app>"` way
|
|
73
|
-
// home
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
+
// home, for a host that needs a real `<a href>` (a server-rendered link, a
|
|
77
|
+
// middle-click). For a button, prefer `useConnect().openConnect`, which also
|
|
78
|
+
// brings the visitor back when the task is done.
|
|
76
79
|
var portal_links_js_1 = require("./portal-links.js");
|
|
77
80
|
Object.defineProperty(exports, "connectPortalUrl", { enumerable: true, get: function () { return portal_links_js_1.connectPortalUrl; } });
|
|
78
81
|
Object.defineProperty(exports, "useConnectPortalUrl", { enumerable: true, get: function () { return portal_links_js_1.useConnectPortalUrl; } });
|
|
@@ -22,6 +22,8 @@ exports.useConnectResource = useConnectResource;
|
|
|
22
22
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
23
23
|
const react_1 = require("react");
|
|
24
24
|
const one_tap_state_js_1 = require("./components/one-tap-state.js");
|
|
25
|
+
const errors_js_1 = require("../errors.js");
|
|
26
|
+
const handoff_js_1 = require("./handoff.js");
|
|
25
27
|
exports.CONNECT_APP_URL = "https://connect.cosmoops.com";
|
|
26
28
|
const ConnectContext = (0, react_1.createContext)(null);
|
|
27
29
|
/** Marks that this tab has already spent its one silent attempt - see `ConnectProviderProps.silentSignIn`. */
|
|
@@ -40,7 +42,7 @@ const SILENT_ATTEMPT_KEY = "connect_silent_attempted";
|
|
|
40
42
|
* </ConnectProvider>
|
|
41
43
|
* ```
|
|
42
44
|
*/
|
|
43
|
-
function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, silentSignIn = false, children, }) {
|
|
45
|
+
function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = exports.CONNECT_APP_URL, fetch: fetchImpl, silentSignIn = false, handoff = "popup", onHandoffResult, children, }) {
|
|
44
46
|
// One state cell, not three. `isLoading` is *derived* from "no result yet"
|
|
45
47
|
// rather than set - a `setIsLoading(true)` at the top of an effect is a
|
|
46
48
|
// synchronous setState inside that effect, which cascades an extra render
|
|
@@ -142,7 +144,17 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
142
144
|
});
|
|
143
145
|
if (!response.ok) {
|
|
144
146
|
const envelope = await response.json().catch(() => null);
|
|
145
|
-
|
|
147
|
+
// A `ConnectError` rather than a bare `Error`, so a caller can branch
|
|
148
|
+
// on the status and code - a signed-out 401 and a throttled 429 need
|
|
149
|
+
// different words in front of a person. Same message as before, so
|
|
150
|
+
// anything already reading it sees no change.
|
|
151
|
+
throw new errors_js_1.ConnectError({
|
|
152
|
+
message: envelope?.error?.message ?? `Connect bridge returned ${response.status}`,
|
|
153
|
+
status: response.status,
|
|
154
|
+
code: typeof envelope?.error?.code === "string" ? envelope.error.code : "unknown",
|
|
155
|
+
endpoint: `POST ${path}`,
|
|
156
|
+
body: envelope,
|
|
157
|
+
});
|
|
146
158
|
}
|
|
147
159
|
return response;
|
|
148
160
|
}, [basePath, fetchImpl]);
|
|
@@ -192,6 +204,61 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
192
204
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
193
205
|
void refresh();
|
|
194
206
|
}, [refresh]);
|
|
207
|
+
/*
|
|
208
|
+
* Hand-offs to Connect - see `ConnectContextValue.openConnect`.
|
|
209
|
+
*
|
|
210
|
+
* The provider's own `clientId` prop wins; otherwise the bridge's, which is
|
|
211
|
+
* what makes this work for the common integration that never passes one.
|
|
212
|
+
*/
|
|
213
|
+
const handoffClientId = clientId || session?.clientId || "";
|
|
214
|
+
const openConnect = (0, react_1.useCallback)((target, options = {}) => {
|
|
215
|
+
const returnTo = window.location.href;
|
|
216
|
+
if (handoff === "popup") {
|
|
217
|
+
// Deliberately no `noopener`: with it, `window.open` returns `null`
|
|
218
|
+
// by specification and Connect gets no `opener` - so it could
|
|
219
|
+
// neither tell this page it finished nor close itself (a browser
|
|
220
|
+
// only lets script close a window script opened). The opener it is
|
|
221
|
+
// handed is this page, and cross-origin that permits navigating it
|
|
222
|
+
// and posting to it - neither of which Connect does uninvited.
|
|
223
|
+
const opened = window.open((0, handoff_js_1.buildHandoffUrl)(target, { appUrl, clientId: handoffClientId, mode: "popup", returnTo }), options.name ?? "_blank", options.features);
|
|
224
|
+
if (opened)
|
|
225
|
+
return opened;
|
|
226
|
+
// Blocked. A same-tab trip still gets them there and back, where a
|
|
227
|
+
// "your browser blocked the window" note leaves them to sort it out.
|
|
228
|
+
}
|
|
229
|
+
window.location.assign((0, handoff_js_1.buildHandoffUrl)(target, { appUrl, clientId: handoffClientId, mode: "redirect", returnTo }));
|
|
230
|
+
return null;
|
|
231
|
+
}, [appUrl, handoff, handoffClientId]);
|
|
232
|
+
// Latest callback without re-subscribing the listener on every render of a
|
|
233
|
+
// host that passes an inline function.
|
|
234
|
+
const onHandoffResultRef = (0, react_1.useRef)(onHandoffResult);
|
|
235
|
+
(0, react_1.useEffect)(() => {
|
|
236
|
+
onHandoffResultRef.current = onHandoffResult;
|
|
237
|
+
});
|
|
238
|
+
(0, react_1.useEffect)(() => {
|
|
239
|
+
// Back from a redirect-mode trip. The page is a fresh load, so every read
|
|
240
|
+
// is already fresh - this only reports the outcome and tidies the URL.
|
|
241
|
+
// `replaceState` rather than a router call: this package imports no
|
|
242
|
+
// router, and Next's App Router honours a native `replaceState`.
|
|
243
|
+
const returned = (0, handoff_js_1.takeHandoffResult)(window.location.href);
|
|
244
|
+
if (returned) {
|
|
245
|
+
window.history.replaceState(window.history.state, "", returned.cleaned);
|
|
246
|
+
onHandoffResultRef.current?.(returned.result);
|
|
247
|
+
}
|
|
248
|
+
// A popup reporting in before it closes. `readHandoffMessage` checks the
|
|
249
|
+
// origin, which is what stops any other window driving this.
|
|
250
|
+
const onMessage = (event) => {
|
|
251
|
+
const result = (0, handoff_js_1.readHandoffMessage)(event, appUrl);
|
|
252
|
+
if (!result)
|
|
253
|
+
return;
|
|
254
|
+
// Session-wide: the plan, the readiness, the seat count - whatever the
|
|
255
|
+
// trip changed, every Connect component on the page re-reads it.
|
|
256
|
+
void refresh();
|
|
257
|
+
onHandoffResultRef.current?.(result);
|
|
258
|
+
};
|
|
259
|
+
window.addEventListener("message", onMessage);
|
|
260
|
+
return () => window.removeEventListener("message", onMessage);
|
|
261
|
+
}, [appUrl, refresh]);
|
|
195
262
|
(0, react_1.useEffect)(() => {
|
|
196
263
|
// `silentPending` already encodes settled-and-signed-out, no prior
|
|
197
264
|
// attempt, and no bridge error - a failed bridge read says nothing about
|
|
@@ -313,8 +380,10 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
313
380
|
}
|
|
314
381
|
}, [fetchImpl, mutateResource, oneTapEndpoints, refresh]);
|
|
315
382
|
const value = (0, react_1.useMemo)(() => ({
|
|
316
|
-
clientId,
|
|
383
|
+
clientId: handoffClientId,
|
|
317
384
|
appUrl,
|
|
385
|
+
handoffMode: handoff,
|
|
386
|
+
openConnect,
|
|
318
387
|
session,
|
|
319
388
|
isLoading,
|
|
320
389
|
error,
|
|
@@ -328,8 +397,10 @@ function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri
|
|
|
328
397
|
readOneTapHint,
|
|
329
398
|
oneTapSignIn,
|
|
330
399
|
}), [
|
|
331
|
-
|
|
400
|
+
handoffClientId,
|
|
332
401
|
appUrl,
|
|
402
|
+
handoff,
|
|
403
|
+
openConnect,
|
|
333
404
|
session,
|
|
334
405
|
isLoading,
|
|
335
406
|
error,
|
|
@@ -5,6 +5,7 @@ exports.useConnectCheckout = useConnectCheckout;
|
|
|
5
5
|
exports.buildCancelUrl = buildCancelUrl;
|
|
6
6
|
exports.buildCheckoutUrl = buildCheckoutUrl;
|
|
7
7
|
const react_1 = require("react");
|
|
8
|
+
const handoff_js_1 = require("./handoff.js");
|
|
8
9
|
const provider_js_1 = require("./provider.js");
|
|
9
10
|
/** How often `/entitlements` is re-read while a checkout is open. */
|
|
10
11
|
const POLL_INTERVAL_MS = 3_000;
|
|
@@ -35,7 +36,7 @@ const CLOSE_GRACE_MS = 12_000;
|
|
|
35
36
|
* period rather than ending the run.
|
|
36
37
|
*/
|
|
37
38
|
function useConnectCheckout() {
|
|
38
|
-
const { fetchResource, invalidateResource, refresh } = (0, provider_js_1.useConnect)();
|
|
39
|
+
const { appUrl, fetchResource, invalidateResource, refresh, openConnect } = (0, provider_js_1.useConnect)();
|
|
39
40
|
const [state, setState] = (0, react_1.useState)({ status: "idle", url: null });
|
|
40
41
|
// Everything the poll loop needs lives in a ref: it runs from a timer, and
|
|
41
42
|
// re-creating the loop whenever a piece of it changed identity would
|
|
@@ -56,20 +57,18 @@ function useConnectCheckout() {
|
|
|
56
57
|
// an await is what pop-up blockers exist to stop, and it would be
|
|
57
58
|
// blocked in every browser regardless of the user's settings.
|
|
58
59
|
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
setState({ status: "blocked", url });
|
|
60
|
+
// Through `openConnect`, which stamps the way back on the URL: Connect
|
|
61
|
+
// then closes this window itself once the change lands, instead of
|
|
62
|
+
// leaving the customer on a billing page they have to close by hand.
|
|
63
|
+
// The handle is still what the grace period below watches.
|
|
64
|
+
//
|
|
65
|
+
// `null` means it did not open a window - the provider is in `redirect`
|
|
66
|
+
// mode, or the popup was blocked and it fell back to one - and this
|
|
67
|
+
// page is navigating away. Nothing to watch; the trip back is a fresh
|
|
68
|
+
// load that reads the new entitlement on its own.
|
|
69
|
+
const opened = openConnect(url, { name: "connect_checkout", features: "popup,width=520,height=780" });
|
|
70
|
+
if (!opened)
|
|
71
71
|
return;
|
|
72
|
-
}
|
|
73
72
|
run.current = {
|
|
74
73
|
url,
|
|
75
74
|
isSettled,
|
|
@@ -79,7 +78,7 @@ function useConnectCheckout() {
|
|
|
79
78
|
generation: generation.current,
|
|
80
79
|
};
|
|
81
80
|
setState({ status: "waiting", url });
|
|
82
|
-
}, [stop]);
|
|
81
|
+
}, [openConnect, stop]);
|
|
83
82
|
(0, react_1.useEffect)(() => {
|
|
84
83
|
// One interval for the hook's whole life rather than one per run: it does
|
|
85
84
|
// nothing while `run.current` is null, and a run that starts from a press
|
|
@@ -135,11 +134,19 @@ function useConnectCheckout() {
|
|
|
135
134
|
void poll();
|
|
136
135
|
};
|
|
137
136
|
document.addEventListener("visibilitychange", onVisible);
|
|
137
|
+
// Connect saying it is done is a stronger hint still - read now, before
|
|
138
|
+
// the window has even finished closing.
|
|
139
|
+
const onMessage = (event) => {
|
|
140
|
+
if ((0, handoff_js_1.readHandoffMessage)(event, appUrl))
|
|
141
|
+
void poll();
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("message", onMessage);
|
|
138
144
|
return () => {
|
|
139
145
|
clearInterval(interval);
|
|
140
146
|
document.removeEventListener("visibilitychange", onVisible);
|
|
147
|
+
window.removeEventListener("message", onMessage);
|
|
141
148
|
};
|
|
142
|
-
}, [fetchResource, invalidateResource, refresh, stop]);
|
|
149
|
+
}, [appUrl, fetchResource, invalidateResource, refresh, stop]);
|
|
143
150
|
return { ...state, start, reset };
|
|
144
151
|
}
|
|
145
152
|
/**
|
|
@@ -25,6 +25,7 @@ const sessions_js_1 = require("./resources/sessions.js");
|
|
|
25
25
|
const platform_js_1 = require("./resources/platform.js");
|
|
26
26
|
const visitor_js_1 = require("./resources/visitor.js");
|
|
27
27
|
const push_js_1 = require("./resources/push.js");
|
|
28
|
+
const whatsapp_js_1 = require("./resources/whatsapp.js");
|
|
28
29
|
exports.DEFAULT_BASE_URL = "https://connect.cosmoops.com";
|
|
29
30
|
const APPLICATION_API_PREFIX = "/api/v1/application";
|
|
30
31
|
/**
|
|
@@ -128,6 +129,8 @@ class ConnectClient {
|
|
|
128
129
|
* preferences; you supply a title, a body and a URL. See `PushResource`.
|
|
129
130
|
*/
|
|
130
131
|
push;
|
|
132
|
+
/** Your application's own approved WhatsApp templates, to a workspace's customers. */
|
|
133
|
+
whatsapp;
|
|
131
134
|
/**
|
|
132
135
|
* Relays one `/data/*` or `/kv/*` request verbatim - `createConnectHandler`'s
|
|
133
136
|
* store route and nothing else. See `storePassthrough`.
|
|
@@ -174,9 +177,10 @@ class ConnectClient {
|
|
|
174
177
|
this.visitor = new visitor_js_1.VisitorResource(transport);
|
|
175
178
|
this.platform = new platform_js_1.PlatformResource(transport);
|
|
176
179
|
this.push = new push_js_1.PushResource(transport);
|
|
180
|
+
this.whatsapp = new whatsapp_js_1.WhatsAppResource(transport);
|
|
177
181
|
this.storeRelay = (spec) => (0, data_js_1.storePassthrough)(transport, spec);
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
exports.ConnectClient = ConnectClient;
|
|
181
185
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
182
|
-
exports.SDK_VERSION = "0.
|
|
186
|
+
exports.SDK_VERSION = "0.43.0";
|
|
@@ -411,6 +411,8 @@ function createConnectHandler(options) {
|
|
|
411
411
|
* anyone who used it).
|
|
412
412
|
*/
|
|
413
413
|
const oneTapEndpoints = session.write ? client.identity.oneTapEndpoints() : null;
|
|
414
|
+
/** See `IdentityResource.publicClientId` - what lets the browser name this application on a hand-off. */
|
|
415
|
+
const clientId = client.identity.publicClientId();
|
|
414
416
|
const routes = {
|
|
415
417
|
"GET /session": async (request) => {
|
|
416
418
|
const token = await session.read(request);
|
|
@@ -432,9 +434,10 @@ function createConnectHandler(options) {
|
|
|
432
434
|
// whose shape depends on the answer is a response consumers start
|
|
433
435
|
// testing for key presence instead of value.
|
|
434
436
|
oneTap: oneTapEndpoints,
|
|
437
|
+
clientId,
|
|
435
438
|
});
|
|
436
439
|
}
|
|
437
|
-
return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints });
|
|
440
|
+
return json({ ...(await client.identity.getUser(token)), oneTap: oneTapEndpoints, clientId });
|
|
438
441
|
},
|
|
439
442
|
"GET /entitlements": async (request) => {
|
|
440
443
|
const { workspaceId } = await requireWorkspace(request);
|
|
@@ -60,8 +60,11 @@ class CallsResource {
|
|
|
60
60
|
* All three need the `calls:place` scope **and** a signed-in member
|
|
61
61
|
* (`asUser`), whose own "Make calls" permission Connect checks. None works
|
|
62
62
|
* server-to-server: a call is placed by a person, from the workspace's own
|
|
63
|
-
* number
|
|
64
|
-
*
|
|
63
|
+
* number. It is recorded only when the workspace has switched recording on
|
|
64
|
+
* (the person called hears a notice); `call.analyzed` then follows with the
|
|
65
|
+
* summary, intent and action items. Read how one went with `get(callId)`,
|
|
66
|
+
* which needs `calls:read` - a call your application placed is readable by
|
|
67
|
+
* it, transcript included.
|
|
65
68
|
*/
|
|
66
69
|
/**
|
|
67
70
|
* Whether the member can call, and if not why - what `<ConnectCallButton>`
|
|
@@ -94,6 +94,18 @@ class IdentityResource {
|
|
|
94
94
|
authorize.searchParams.set("client_id", this.clientId);
|
|
95
95
|
return { hintUrl: hint.toString(), authorizeUrl: authorize.toString() };
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* This client's public `client_id`, or `null` when none is configured.
|
|
99
|
+
*
|
|
100
|
+
* The bridge hands it to the browser on `/session` so `openConnect` can name
|
|
101
|
+
* the application on every hand-off without the host passing `clientId` to
|
|
102
|
+
* `<ConnectProvider>` too - Connect only honours a `return_to` it can match
|
|
103
|
+
* against *a client's* registered redirect origins. Public by definition:
|
|
104
|
+
* it already rides on every authorize URL.
|
|
105
|
+
*/
|
|
106
|
+
publicClientId() {
|
|
107
|
+
return this.clientId;
|
|
108
|
+
}
|
|
97
109
|
/** Step 2 - trade the one-time `code` for a session. Server-side only. */
|
|
98
110
|
async exchangeCode(input) {
|
|
99
111
|
const wire = await this.post("/sso/token", {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WhatsAppResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* Your application's own WhatsApp templates, sent to a workspace's customers
|
|
7
|
+
* through Connect's WhatsApp sender - no WhatsApp Business account of your
|
|
8
|
+
* own.
|
|
9
|
+
*
|
|
10
|
+
* Templates are written and submitted to Meta in Console under your
|
|
11
|
+
* application; you name them here by slug and Connect resolves
|
|
12
|
+
* `app_<yourApplicationKey>_<slug>` from your key, so a key can never send
|
|
13
|
+
* another application's message or one of Connect's own. There is no free
|
|
14
|
+
* text: outside a customer's 24-hour window WhatsApp permits nothing else.
|
|
15
|
+
*
|
|
16
|
+
* Server-to-server only - there is no browser path, by design.
|
|
17
|
+
*/
|
|
18
|
+
class WhatsAppResource {
|
|
19
|
+
transport;
|
|
20
|
+
constructor(transport) {
|
|
21
|
+
this.transport = transport;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Send one template to one customer.
|
|
25
|
+
*
|
|
26
|
+
* Refusals arrive as a `ConnectError` whose `code` says what to tell the
|
|
27
|
+
* person who pressed Send - `template_not_approved`, `consent_required`,
|
|
28
|
+
* `opted_out`, `not_found`. None of them is a fault worth retrying.
|
|
29
|
+
*
|
|
30
|
+
* An `Idempotency-Key` is always sent, as on `otp.send`: a retried send
|
|
31
|
+
* without one is a second message on somebody's phone. Pass your own -
|
|
32
|
+
* derived from what is being sent, e.g. the quote number - to make a
|
|
33
|
+
* retry from a *later* request safe too.
|
|
34
|
+
*/
|
|
35
|
+
send(input, options = {}) {
|
|
36
|
+
return (0, http_js_1.request)(this.transport, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
path: "/whatsapp/messages",
|
|
39
|
+
json: input,
|
|
40
|
+
headers: { "idempotency-key": options.idempotencyKey ?? crypto.randomUUID() },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether a template would send right now, and the values it needs.
|
|
45
|
+
*
|
|
46
|
+
* Ask before offering WhatsApp on a screen: Meta's approval takes minutes
|
|
47
|
+
* to days, and an approved template can be paused later. A `ConnectError`
|
|
48
|
+
* with `status: 404` means your application has no such template.
|
|
49
|
+
*/
|
|
50
|
+
template(slug, options = {}) {
|
|
51
|
+
const language = options.language ? `?language=${encodeURIComponent(options.language)}` : "";
|
|
52
|
+
return (0, http_js_1.request)(this.transport, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
path: `/whatsapp/templates/${encodeURIComponent(slug)}${language}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.WhatsAppResource = WhatsAppResource;
|