@cosmoops/connect-sdk 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +251 -0
  2. package/README.md +17 -5
  3. package/dist/cjs/client/components/access-gate.js +98 -2
  4. package/dist/cjs/client/components/visitor-chat.js +387 -0
  5. package/dist/cjs/client/components/visitor-stream.js +124 -0
  6. package/dist/cjs/client/index.js +16 -3
  7. package/dist/cjs/client/provider.js +29 -2
  8. package/dist/cjs/client/use-checkout.js +3 -1
  9. package/dist/cjs/helpers.js +46 -0
  10. package/dist/cjs/index.js +5 -1
  11. package/dist/cjs/next/index.js +10 -3
  12. package/dist/cjs/server/client.js +8 -1
  13. package/dist/cjs/server/handler.js +142 -0
  14. package/dist/cjs/server/resources/ai.js +50 -0
  15. package/dist/cjs/server/resources/billing.js +5 -0
  16. package/dist/cjs/server/resources/enrichment.js +59 -0
  17. package/dist/cjs/server/resources/entitlements.js +47 -0
  18. package/dist/cjs/server/resources/visitor.js +227 -0
  19. package/dist/esm/client/components/access-gate.js +99 -3
  20. package/dist/esm/client/components/visitor-chat.js +384 -0
  21. package/dist/esm/client/components/visitor-stream.js +120 -0
  22. package/dist/esm/client/index.js +11 -1
  23. package/dist/esm/client/provider.js +28 -2
  24. package/dist/esm/client/use-checkout.js +3 -1
  25. package/dist/esm/helpers.js +43 -0
  26. package/dist/esm/index.js +1 -0
  27. package/dist/esm/next/index.js +10 -3
  28. package/dist/esm/server/client.js +8 -1
  29. package/dist/esm/server/handler.js +142 -0
  30. package/dist/esm/server/resources/ai.js +50 -0
  31. package/dist/esm/server/resources/billing.js +5 -0
  32. package/dist/esm/server/resources/enrichment.js +59 -0
  33. package/dist/esm/server/resources/entitlements.js +47 -0
  34. package/dist/esm/server/resources/visitor.js +222 -0
  35. package/dist/types/client/components/access-gate.d.ts +9 -0
  36. package/dist/types/client/components/visitor-chat.d.ts +120 -0
  37. package/dist/types/client/components/visitor-stream.d.ts +42 -0
  38. package/dist/types/client/index.d.ts +6 -1
  39. package/dist/types/client/provider.d.ts +176 -51
  40. package/dist/types/client/use-checkout.d.ts +18 -1
  41. package/dist/types/helpers.d.ts +36 -1
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/server/client.d.ts +7 -1
  44. package/dist/types/server/handler.d.ts +0 -24
  45. package/dist/types/server/resources/ai.d.ts +48 -1
  46. package/dist/types/server/resources/billing.d.ts +4 -0
  47. package/dist/types/server/resources/enrichment.d.ts +57 -1
  48. package/dist/types/server/resources/entitlements.d.ts +40 -1
  49. package/dist/types/server/resources/visitor.d.ts +200 -0
  50. package/dist/types/types.d.ts +562 -0
  51. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,257 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.34.0
7
+
8
+ Getting a blocked customer unblocked, and getting them back afterwards - plus
9
+ the field the deletion events were missing. No new resources: two of these are
10
+ the places the subscription journey dead-ended, and the third is a number your
11
+ webhook handler had no way to read.
12
+
13
+ ### `<ConnectAccessGate>` no longer strands people in a second tab
14
+
15
+ The `subscription_missing` card's button used to `window.open(resolveUrl,
16
+ "_blank")`. The customer subscribed over there, was left on Connect's billing
17
+ page with nothing pointing home, and the tab they started in still showed
18
+ "Choose a plan to continue" - unchanged, because nothing re-reads entitlements
19
+ on its own. The usual ending was two open tabs and the belief that the payment
20
+ had not gone through.
21
+
22
+ Now it is a **same-tab navigation carrying `?next=`** back to the page they
23
+ were standing on. Connect honours it after the subscribe when the origin
24
+ matches one registered for your Connect client, so the customer lands back
25
+ exactly where they were, subscribed.
26
+
27
+ Register your app's origin in Console if you have not - an unregistered one
28
+ costs the return trip, nothing more. They still reach Connect, still resolve
29
+ the blocker, and:
30
+
31
+ **A blocked gate now re-checks when the tab regains focus.** Covers everything
32
+ the return trip cannot: a second tab, a colleague with the admin rights, a
33
+ payment settled on a phone. Only while blocked - a `clear` gate does not put a
34
+ request on the critical path of alt-tabbing back into a working app.
35
+
36
+ `useConnectAccessGate()` gained a `refresh()` for the cases your app learns of
37
+ a change some other way.
38
+
39
+ ### `buildCheckoutUrl({ next })`
40
+
41
+ Where Connect sends the customer once the subscription is live:
42
+
43
+ ```ts
44
+ buildCheckoutUrl({
45
+ appUrl,
46
+ applicationId,
47
+ planId,
48
+ next: window.location.href, // back to your pricing page, subscribed
49
+ });
50
+ ```
51
+
52
+ Without it the default destination is the new subscription's own billing page
53
+ - right for someone who started in Connect, wrong for someone who started on
54
+ your pricing page. Connect validates the origin against your registered
55
+ redirect URIs and ignores anything else, so an unrecognised value costs the
56
+ return trip and can never redirect your customer somewhere you did not
57
+ register.
58
+
59
+ ### `useConnectResource().refetch()` actually refetches
60
+
61
+ It only re-ran the effect, on the assumption the caller had just evicted the
62
+ cache entry itself through `mutateResource`. The provider's cache is a `Map`
63
+ of resolved promises with no expiry, so a `refetch()` on its own re-read the
64
+ same settled promise and reported the same stale answer forever. It now evicts
65
+ first. Still safe after a `mutateResource` against the same path.
66
+
67
+ ### Note for applications that cannot be used without a plan
68
+
69
+ If your application is unusable without a subscription, the better fix is on
70
+ Connect's side and needs no SDK change: a platform admin turns on **"Require a
71
+ subscription to sign in"** on your Connect client. Plan selection then happens
72
+ *inside* the sign-in handshake - the customer picks a plan, free tier first,
73
+ and is handed back to you automatically - instead of them arriving signed-in
74
+ and hitting this gate at all. The gate stays as the backstop for a
75
+ subscription that lapses mid-use, which is what the changes above are for.
76
+
77
+
78
+ ### The deletion events carry your retention policy
79
+
80
+ `workspace.deletion_scheduled` and `workspace.deleted` now include `policy`:
81
+ your application's own data policy, frozen as it stood when the customer
82
+ confirmed the deletion.
83
+
84
+ ```ts
85
+ "workspace.deleted": async ({ workspaceId, policy }) => {
86
+ for (const dataClass of policy?.dataClasses ?? []) {
87
+ if (dataClass.disposition === "retain") {
88
+ await archive(workspaceId, dataClass.key, dataClass.retainDays);
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ Before this, the two events carried `purgeAfter` and `graceDays` — Connect's
95
+ clock — and nothing about yours. The number a handler actually acts on is
96
+ `retainDays` per data class (and `quarantineDays` per held identifier), and
97
+ that was the one number never on the wire: every integration had to re-declare
98
+ in code what it had already declared in Console, where the two drift the first
99
+ time somebody edits the policy and nothing fails.
100
+
101
+ `policy` is per recipient — a deletion fans out to every application the
102
+ workspace subscribed to, and each receives only its own declaration. It is the
103
+ *clamped*, *versioned* snapshot: `version` names the policy the customer was
104
+ shown, and the numbers are the ones Connect promised them rather than whatever
105
+ your stored policy says today. `null` only for a deletion confirmed before
106
+ policies existed; an application that has declared nothing gets empty arrays,
107
+ never a missing key.
108
+
109
+ New types: `ConnectDeletionPolicyNotice`, `ConnectDataClassNotice`,
110
+ `ConnectResourceReleaseNotice`, `ConnectDataDisposition`,
111
+ `ConnectFiduciaryRole`.
112
+
113
+ Also corrected: `requestedBy` on `workspace.deletion_scheduled` is
114
+ `"owner" | "platform_admin"` — which path the request came in on. The payload
115
+ example in Console had been showing a user id, which is not on the wire.
116
+
117
+ ## 0.33.0
118
+
119
+ Two additions: the public visitor assistant, and `connect.enrichment.presence()`.
120
+
121
+ ### The visitor assistant
122
+
123
+ A chat bubble for **your customers' own websites**, answered by Connect, for
124
+ people who have no account anywhere. A visitor asks about that business, or
125
+ books a call with them; your application holds no model credential and runs no
126
+ AI infrastructure.
127
+
128
+ Three calls drive a conversation and five manage what it is allowed to say:
129
+
130
+ ```ts
131
+ const session = await connect.visitor.startSession({
132
+ workspaceId, // which of your customers' sites this is
133
+ page: "/pricing",
134
+ });
135
+
136
+ const stream = await connect.visitor.chat({
137
+ sessionId: session.sessionId,
138
+ workspaceId,
139
+ message: "do you take walk-ins?",
140
+ });
141
+ ```
142
+
143
+ Needs an API key scoped to **`visitor:chat`**, and **`visitor:knowledge`** to
144
+ manage the knowledge base. Both are workspace-scoped, so they carry the second
145
+ check every workspace-wide scope carries: a live subscription to the workspace
146
+ you name.
147
+
148
+ **The assistant answers from a knowledge base you publish, and from nothing
149
+ else.** `connect.visitor.knowledge.*` writes to a store that exists only for
150
+ this purpose:
151
+
152
+ ```ts
153
+ const item = await connect.visitor.knowledge.save({
154
+ workspaceId,
155
+ title: "Opening hours",
156
+ body: "Mon-Fri, 9 to 6. Closed on public holidays.",
157
+ source: "authored",
158
+ });
159
+ await connect.visitor.knowledge.publish({ workspaceId, itemId: item.id });
160
+ ```
161
+
162
+ Five things to know before wiring it in, in the order they will bite you:
163
+
164
+ **It is not the workspace's Context Index, and must never become it.** The
165
+ corpus behind `ai.chat` is fed by every Drive upload a workspace makes —
166
+ contracts, payroll, customer lists. Pointing a public widget at that would let
167
+ a stranger extract an internal document, helpfully summarised. Nothing reaches
168
+ the visitor store except through an explicit `publish`, and the assistant's
169
+ read path has no route to the other corpus at all. Do not try to fill it by
170
+ uploading to Drive; that is the invariant, not an inconvenience.
171
+
172
+ **Publishing is its own call, and unpublishing can fail.** `save` always writes
173
+ a draft, so no edit can make text public by accident. `unpublish` removes the
174
+ retrievable copy *before* it flips the record, which means a failure leaves the
175
+ item **still live** — surface the error and let them retry rather than greying
176
+ the row out optimistically. For a retracted price, an item that looks withdrawn
177
+ and is not is the most expensive bug this feature has.
178
+
179
+ **Record `grounded: false` from the `done` event.** It means nothing published
180
+ covered the question, so the assistant said so and offered a call instead of
181
+ inventing an answer. A run of them is a knowledge base with a gap in it, which
182
+ is the most useful signal this endpoint produces.
183
+
184
+ **`mode` tells you how the knowledge reaches the model**, and `inline` is the
185
+ good state. Below a size threshold the whole published set is carried in the
186
+ prompt — the model sees every item rather than a top-k slice, with no chunk
187
+ boundaries and no retrieval round trip. `retrieval` only begins once a
188
+ knowledge base has outgrown that, and only then does `indexState` on each item
189
+ matter.
190
+
191
+ **The workspace is yours to resolve, not the browser's.** Connect checks the
192
+ `workspaceId` you send against your own subscriptions, so the worst case is
193
+ bounded to your own customers rather than the platform's. That is still one
194
+ customer's budget spent from another's page, so if you serve several customers,
195
+ resolve the workspace from your own widget record — the token in the embed
196
+ snippet, say — rather than trusting a field a page supplied.
197
+
198
+ `<ConnectVisitorChat>` in `./client` is the widget itself: a bubble that
199
+ becomes a full-height sheet on a phone and an anchored panel above `sm`, with
200
+ the composer's input type decided by the server rather than guessed from the
201
+ reply — so an email field brings up a keyboard with an `@` on it and a
202
+ verification code can be offered straight from the notification. It renders on
203
+ a page with **no `ConnectProvider`**, which most marketing pages are, and it
204
+ mints nothing until somebody opens it.
205
+
206
+ Times offered for a call are drawn from the turn's tool result, never from the
207
+ assistant's sentence, and the appointment is placed by a typed action rather
208
+ than by the model — so it cannot book a time nobody offered, and a phone number
209
+ never arrives parsed out of prose.
210
+
211
+ ### `connect.enrichment.presence()`
212
+
213
+ `connect.enrichment.presence()` — how visible a business is online, scored out
214
+ of 100.
215
+
216
+ The third sibling of `company()` and `brand()`: that one answers what a
217
+ business does, the other what it looks like, and this answers how findable it
218
+ is — by a search engine, by a customer, and by the assistants that
219
+ increasingly answer questions about a business before anybody visits its site.
220
+
221
+ Returns the score, a breakdown across five categories, every check with the
222
+ evidence behind it, every social profile linked from the site, and a
223
+ prioritised list of what to fix.
224
+
225
+ ```ts
226
+ const result = await connect.enrichment.presence({ domain: "acme.com" });
227
+ if (result.status === "scanned") {
228
+ for (const next of result.scan.recommendations) {
229
+ console.log(`${next.priority}. ${next.title} — ${next.fix}`);
230
+ }
231
+ }
232
+ ```
233
+
234
+ Needs an API key scoped to **`enrichment:presence`**.
235
+
236
+ Three things to know before wiring it in:
237
+
238
+ **The score is computed, not generated.** Every point comes from a check run
239
+ against something a fetcher actually observed, and every check ships with its
240
+ weight, what was seen and the URL that settles it — so the total is
241
+ reconstructable. A model only orders the recommendations and writes the prose;
242
+ it cannot move the number. That is the whole reason this is worth showing a
243
+ customer: ask a model how good a company's web presence is and it answers 72
244
+ with total confidence for a site it never read.
245
+
246
+ **`social` is claimed, never verified.** Those are URLs the site itself
247
+ printed. Nothing proves the account belongs to that business — render it as
248
+ "linked from their site". Check `machineReadable` too: `false` means a real
249
+ profile that search engines have to infer from a footer link, which is a
250
+ specific, common and easily fixed finding.
251
+
252
+ **A low score is a success, not a skip.** Every check failing is the most
253
+ useful report this produces. Only `unreachable` means the site did not answer —
254
+ and `provider_unavailable` means *Connect* did not, which is never something
255
+ to tell your user about their own site.
256
+
6
257
  ## 0.32.0
7
258
 
8
259
  Push notifications: `connect.push.send()` on the server, `useConnectPush()` in
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official client for the **CosmoOps Connect application-partner API** (`/api/v1/application/*`).
4
4
 
5
- **Current version: `0.20.0`** · [Release notes](#release-notes) · [Full changelog](./CHANGELOG.md)
5
+ **Current version: `0.33.0`** · [Release notes](#release-notes) · [Full changelog](./CHANGELOG.md)
6
6
  — follows semver with the 0.x convention that a **minor** version may carry a
7
7
  breaking change (a patch never does). Read the release notes for your target
8
8
  version before upgrading past a minor bump.
@@ -12,7 +12,7 @@ npm install @cosmoops/connect-sdk
12
12
  npm install streamdown # optional — Ask AI's markdown renderer, see step 4
13
13
  ```
14
14
 
15
- Five entry points, and the split is a security boundary, not a preference:
15
+ Six entry points, and the split is a security boundary, not a preference:
16
16
 
17
17
  | Import | Runs in | Holds the API key |
18
18
  | --- | --- | --- |
@@ -20,8 +20,12 @@ Five entry points, and the split is a security boundary, not a preference:
20
20
  | `@cosmoops/connect-sdk/client` | the browser | never |
21
21
  | `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
22
22
  | `@cosmoops/connect-sdk/mcp` | your backend | **yes** (store, as MCP tools) |
23
+ | `@cosmoops/connect-sdk/ui` | either | never (presentational, no `"use client"`) |
23
24
  | `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
24
25
 
26
+ Plus `@cosmoops/connect-sdk/theme.css`, which is not code: the one shared
27
+ HeroUI theme the components are drawn against.
28
+
25
29
  The browser half never sees a credential. It calls your own origin, and
26
30
  `createConnectHandler` — mounted in your backend — is what talks to Connect.
27
31
 
@@ -49,8 +53,14 @@ npm install @cosmoops/connect-sdk
49
53
  | --- | --- | --- |
50
54
  | `CONNECT_API_KEY` | yes | server-side credential |
51
55
  | `CONNECT_CLIENT_ID` | yes | OAuth client id |
52
- | `CONNECT_APP_URL` | no | non-production Connect instance |
53
- | `CONNECT_SESSION_COOKIE` | no | session cookie name |
56
+ | `CONNECT_APP_URL` | no | a non-production Connect instance; unset means `https://connect.cosmoops.com` |
57
+ | `CONNECT_SESSION_COOKIE` | no | session cookie name; unset means `connect_session` |
58
+
59
+ Production is the absence of configuration — set `CONNECT_APP_URL` only when
60
+ you mean somewhere else. Note that it is **server-side**: `ConnectProvider`
61
+ defaults `appUrl` to the production origin on its own, so an app pointed at a
62
+ local instance must pass `appUrl` too or its portal links and its sign-in
63
+ redirect will go to production. See [Environments](https://connect.cosmoops.com/docs/environments).
54
64
 
55
65
  **3. Mount the bridge route.** One file, no arguments — this is your entire backend integration:
56
66
 
@@ -919,7 +929,9 @@ So the hand-off is a URL, and Connect renders it as a modal over the relevant pa
919
929
 
920
930
  ## Release notes
921
931
 
922
- Full history: [CHANGELOG.md](./CHANGELOG.md). Latest first:
932
+ **[CHANGELOG.md](./CHANGELOG.md) is the current and complete history**, through
933
+ `0.33.0`. What follows is an older excerpt, kept for the migration notes in it, and
934
+ it stops at `0.20.0` — read the changelog for anything newer.
923
935
 
924
936
  ### 0.20.0
925
937
 
@@ -40,7 +40,19 @@ function formatDate(iso) {
40
40
  */
41
41
  function useConnectAccessGate() {
42
42
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
43
- const { data, isLoading: entitlementsLoading } = (0, provider_js_1.useConnectEntitlements)();
43
+ const { data, isLoading: entitlementsLoading, refetch } = (0, provider_js_1.useConnectEntitlements)();
44
+ return { ...decideAccess(session, sessionLoading, data, entitlementsLoading), refresh: refetch };
45
+ }
46
+ /**
47
+ * The priority ordering, with the reads lifted out.
48
+ *
49
+ * Split from the hook so the twelve verdicts stay one `return` each and none
50
+ * of them has to carry `refresh` - a field that is the same function in every
51
+ * branch and says nothing about which branch it is. Pure, like
52
+ * `rbac/policy.ts` and `evaluateSignInRisk`, and for the same reason: a
53
+ * decision spread across its inputs cannot be tested.
54
+ */
55
+ function decideAccess(session, sessionLoading, data, entitlementsLoading) {
44
56
  if (sessionLoading) {
45
57
  return { state: "loading", blocker: null, paymentRetry: null };
46
58
  }
@@ -151,6 +163,49 @@ function useConnectAccessGate() {
151
163
  */
152
164
  function ConnectAccessGate({ children, fallback, loading, className }) {
153
165
  const gate = useConnectAccessGate();
166
+ const isBlocked = gate.state !== "loading" &&
167
+ gate.state !== "signed_out" &&
168
+ gate.state !== "clear" &&
169
+ gate.state !== "payment_retry";
170
+ /**
171
+ * A blocked gate re-checks when the tab comes back.
172
+ *
173
+ * Every blocker this component draws is resolved **somewhere else** - on
174
+ * Connect, in another tab, by an admin, on a phone - and none of that
175
+ * reaches this tab. The resolve button used to open Connect in a new tab,
176
+ * so the overwhelmingly common path was: subscribe over there, switch back,
177
+ * and find the same "Choose a plan to continue" card, with a reload the
178
+ * only way out. It read as the purchase not having worked.
179
+ *
180
+ * The button is a same-tab navigation now (see `GateCard`), which handles
181
+ * the common case on its own. This covers the rest: a second tab, a
182
+ * colleague with the admin rights, a payment settled on a phone.
183
+ *
184
+ * Only while blocked, deliberately. A `clear` gate re-reading entitlements
185
+ * on every tab focus would put a request on the critical path of
186
+ * alt-tabbing back into an application that is working fine, for every
187
+ * workspace, all day. Nothing is gained by it: the gate has nothing to
188
+ * show, and losing access mid-session is what the server-side check before
189
+ * each action is for.
190
+ */
191
+ (0, react_1.useEffect)(() => {
192
+ if (!isBlocked || typeof document === "undefined")
193
+ return;
194
+ const recheck = () => {
195
+ if (document.visibilityState === "visible")
196
+ gate.refresh();
197
+ };
198
+ document.addEventListener("visibilitychange", recheck);
199
+ window.addEventListener("focus", recheck);
200
+ return () => {
201
+ document.removeEventListener("visibilitychange", recheck);
202
+ window.removeEventListener("focus", recheck);
203
+ };
204
+ // `gate.refresh` is stable per render of the provider and re-subscribing
205
+ // on it would churn the listeners; `isBlocked` is the only thing that
206
+ // should add or remove them.
207
+ // eslint-disable-next-line react-hooks/exhaustive-deps
208
+ }, [isBlocked]);
154
209
  // `children`, unless the host explicitly asked for a placeholder. See the
155
210
  // doc comment - this is the branch that decides whether a partner's page
156
211
  // can stream.
@@ -219,9 +274,27 @@ function AccessGateFallback({ state, blocker, className, }) {
219
274
  * so "see plans" landing back on the same blocked screen never gets anyone
220
275
  * to a choice they can act on. Sending them to Connect's own subscribe page
221
276
  * always resolves.
277
+ *
278
+ * ### Same tab, and it carries the way back
279
+ *
280
+ * It used to `window.open(resolveUrl, "_blank")`. A new tab is the wrong
281
+ * shape for a step you have to finish before you can continue: the customer
282
+ * subscribes over there, is left on Connect's billing page with nothing
283
+ * pointing home, and the tab they started in is still showing this card. The
284
+ * most common ending was two open tabs and the belief that the payment had
285
+ * not worked.
286
+ *
287
+ * So: a same-tab navigation carrying `?next=` back to the page they are
288
+ * standing on, which Connect honours after a subscribe once the origin
289
+ * matches one registered for this application's client
290
+ * (`resolveApplicationReturnUrl`). An unregistered origin costs the return
291
+ * trip and nothing else - they still reach Connect, still resolve the
292
+ * blocker, and `ConnectAccessGate`'s focus re-check catches them on the way
293
+ * back. Partners should register their app's origin; the fallback is a
294
+ * degraded path, not a broken one.
222
295
  */
223
296
  function GateCard({ icon: Icon, iconClassName, title, message, actionLabel, resolveUrl, className, }) {
224
- return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: motion_js_1.ENTER, children: (0, jsx_runtime_1.jsxs)(react_2.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_2.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)(react_2.Card.Title, { children: title })] }), (0, jsx_runtime_1.jsx)(react_2.Card.Description, { children: message })] }), resolveUrl && ((0, jsx_runtime_1.jsx)(react_2.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "primary", onPress: () => window.open(resolveUrl, "_blank", "noopener,noreferrer"), children: [actionLabel ?? "Resolve on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) }))] }) }) }));
297
+ return ((0, jsx_runtime_1.jsx)(motion_js_1.MotionProvider, { children: (0, jsx_runtime_1.jsx)(motion_js_1.m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: motion_js_1.ENTER, children: (0, jsx_runtime_1.jsxs)(react_2.Card, { className: className, children: [(0, jsx_runtime_1.jsxs)(react_2.Card.Header, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)(react_2.Card.Title, { children: title })] }), (0, jsx_runtime_1.jsx)(react_2.Card.Description, { children: message })] }), resolveUrl && ((0, jsx_runtime_1.jsx)(react_2.Card.Footer, { children: (0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "primary", onPress: () => window.location.assign(withReturnTo(resolveUrl)), children: [actionLabel ?? "Resolve on Connect", (0, jsx_runtime_1.jsx)(lucide_react_1.ExternalLink, { className: "size-3.5" })] }) }))] }) }) }));
225
298
  }
226
299
  /**
227
300
  * The skippable payment-due warning. Dismissal is scoped to this specific
@@ -240,3 +313,26 @@ function PaymentRetryBanner({ blocker, className, }) {
240
313
  window.localStorage.setItem(dismissKey, "1");
241
314
  }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "size-4" }) })] })] }) }));
242
315
  }
316
+ /**
317
+ * `resolveUrl` with `?next=` pointing at the page the customer is standing
318
+ * on, so Connect can hand them back once the blocker is gone.
319
+ *
320
+ * Returns the URL untouched if anything is off - no `window`, an unparseable
321
+ * `resolveUrl`, a `next` already set by the server. Getting back is a
322
+ * courtesy on top of resolving the blocker, and it must never be the reason
323
+ * somebody cannot reach the screen that resolves it.
324
+ */
325
+ function withReturnTo(resolveUrl) {
326
+ if (typeof window === "undefined")
327
+ return resolveUrl;
328
+ try {
329
+ const url = new URL(resolveUrl);
330
+ if (url.searchParams.has("next"))
331
+ return resolveUrl;
332
+ url.searchParams.set("next", window.location.href);
333
+ return url.toString();
334
+ }
335
+ catch {
336
+ return resolveUrl;
337
+ }
338
+ }