@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.
- package/CHANGELOG.md +251 -0
- package/README.md +17 -5
- package/dist/cjs/client/components/access-gate.js +98 -2
- package/dist/cjs/client/components/visitor-chat.js +387 -0
- package/dist/cjs/client/components/visitor-stream.js +124 -0
- package/dist/cjs/client/index.js +16 -3
- package/dist/cjs/client/provider.js +29 -2
- package/dist/cjs/client/use-checkout.js +3 -1
- package/dist/cjs/helpers.js +46 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/next/index.js +10 -3
- package/dist/cjs/server/client.js +8 -1
- package/dist/cjs/server/handler.js +142 -0
- package/dist/cjs/server/resources/ai.js +50 -0
- package/dist/cjs/server/resources/billing.js +5 -0
- package/dist/cjs/server/resources/enrichment.js +59 -0
- package/dist/cjs/server/resources/entitlements.js +47 -0
- package/dist/cjs/server/resources/visitor.js +227 -0
- package/dist/esm/client/components/access-gate.js +99 -3
- package/dist/esm/client/components/visitor-chat.js +384 -0
- package/dist/esm/client/components/visitor-stream.js +120 -0
- package/dist/esm/client/index.js +11 -1
- package/dist/esm/client/provider.js +28 -2
- package/dist/esm/client/use-checkout.js +3 -1
- package/dist/esm/helpers.js +43 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/next/index.js +10 -3
- package/dist/esm/server/client.js +8 -1
- package/dist/esm/server/handler.js +142 -0
- package/dist/esm/server/resources/ai.js +50 -0
- package/dist/esm/server/resources/billing.js +5 -0
- package/dist/esm/server/resources/enrichment.js +59 -0
- package/dist/esm/server/resources/entitlements.js +47 -0
- package/dist/esm/server/resources/visitor.js +222 -0
- package/dist/types/client/components/access-gate.d.ts +9 -0
- package/dist/types/client/components/visitor-chat.d.ts +120 -0
- package/dist/types/client/components/visitor-stream.d.ts +42 -0
- package/dist/types/client/index.d.ts +6 -1
- package/dist/types/client/provider.d.ts +176 -51
- package/dist/types/client/use-checkout.d.ts +18 -1
- package/dist/types/helpers.d.ts +36 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/server/client.d.ts +7 -1
- package/dist/types/server/handler.d.ts +0 -24
- package/dist/types/server/resources/ai.d.ts +48 -1
- package/dist/types/server/resources/billing.d.ts +4 -0
- package/dist/types/server/resources/enrichment.d.ts +57 -1
- package/dist/types/server/resources/entitlements.d.ts +40 -1
- package/dist/types/server/resources/visitor.d.ts +200 -0
- package/dist/types/types.d.ts +562 -0
- package/package.json +3 -2
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VisitorKnowledgeResource = exports.VisitorResource = void 0;
|
|
4
|
+
const http_js_1 = require("../http.js");
|
|
5
|
+
/**
|
|
6
|
+
* The header a bridge forwards the visitor's own address on.
|
|
7
|
+
*
|
|
8
|
+
* It exists because `x-forwarded-for` cannot do this job here: by the time your
|
|
9
|
+
* backend calls Connect, that header describes the hop from *your server*, and
|
|
10
|
+
* the address Connect's per-visitor limiter needs is the one on the hop before
|
|
11
|
+
* it - which only your bridge has seen.
|
|
12
|
+
*
|
|
13
|
+
* **What it is and is not trusted for.** It partitions your own application's
|
|
14
|
+
* budget fairly between visitors; it does not grant anything. A caller sending a
|
|
15
|
+
* fresh value per request defeats only the per-visitor limiter and is still
|
|
16
|
+
* bounded by the per-key burst limit and the per-application daily ceiling -
|
|
17
|
+
* which are the two that actually cap spending, and neither is client-supplied.
|
|
18
|
+
* Omit it and every visitor shares one bucket, which is a stricter limit rather
|
|
19
|
+
* than an exemption.
|
|
20
|
+
*/
|
|
21
|
+
const VISITOR_IP_HEADER = "x-connect-visitor-ip";
|
|
22
|
+
function visitorHeaders(options) {
|
|
23
|
+
return options?.visitorIp ? { [VISITOR_IP_HEADER]: options.visitorIp } : {};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The public chat bubble on your own marketing site.
|
|
27
|
+
*
|
|
28
|
+
* The one resource here whose caller is **not** one of your users. There is no
|
|
29
|
+
* `asUser`, no `workspaceId` and no session anywhere in it: the counterpart is a
|
|
30
|
+
* stranger who has no account, and everything the assistant may say or do is
|
|
31
|
+
* configuration a platform admin set on your application record.
|
|
32
|
+
*
|
|
33
|
+
* That is why none of these methods takes an id naming anything. The workspace a
|
|
34
|
+
* call books into, the event type it books, the knowledge it answers from and the
|
|
35
|
+
* daily budget it spends are all read from your application - so nothing a
|
|
36
|
+
* visitor types, and nothing an injection attempt in your own chat window
|
|
37
|
+
* achieves, reaches a different tenant.
|
|
38
|
+
*
|
|
39
|
+
* **Never call these from a browser.** They take your API key, like every other
|
|
40
|
+
* resource on this client. `createConnectHandler` already exposes them on your
|
|
41
|
+
* own origin for `<ConnectVisitorChat>` to use, which is how the credential stays
|
|
42
|
+
* server-side.
|
|
43
|
+
*/
|
|
44
|
+
class VisitorResource {
|
|
45
|
+
transport;
|
|
46
|
+
/**
|
|
47
|
+
* The knowledge base behind the assistant - see `VisitorKnowledgeResource`.
|
|
48
|
+
*
|
|
49
|
+
* Nested rather than a top-level resource, because it is meaningless on its
|
|
50
|
+
* own: it exists to decide what *this* assistant may say.
|
|
51
|
+
*/
|
|
52
|
+
knowledge;
|
|
53
|
+
constructor(transport) {
|
|
54
|
+
this.transport = transport;
|
|
55
|
+
this.knowledge = new VisitorKnowledgeResource(transport);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Open a conversation, or reopen one.
|
|
59
|
+
*
|
|
60
|
+
* Generates nothing - the greeting and the suggested questions come off the
|
|
61
|
+
* application record - so this is the one call in the flow that is always fast,
|
|
62
|
+
* which is what lets a bubble paint real copy instead of a spinner.
|
|
63
|
+
*
|
|
64
|
+
* Pass `resume` with a `sessionId` you kept and the transcript comes back with
|
|
65
|
+
* it. A `resume` that is expired, unknown or another application's silently
|
|
66
|
+
* starts a fresh conversation rather than failing: the token is the only thing
|
|
67
|
+
* being offered and it is being rejected, so there is nothing for the visitor
|
|
68
|
+
* to do about an error.
|
|
69
|
+
*/
|
|
70
|
+
startSession(input, options = {}) {
|
|
71
|
+
return (0, http_js_1.request)(this.transport, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
path: "/visitor/session",
|
|
74
|
+
json: {
|
|
75
|
+
workspaceId: input.workspaceId,
|
|
76
|
+
page: input.page ?? null,
|
|
77
|
+
resume: input.resume ?? null,
|
|
78
|
+
},
|
|
79
|
+
headers: visitorHeaders(options),
|
|
80
|
+
signal: options.signal,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* One turn, streamed.
|
|
85
|
+
*
|
|
86
|
+
* Returns the raw `Response` rather than a parsed body, the same way
|
|
87
|
+
* `ai.chat` does: this answers server-sent events, and buffering them here
|
|
88
|
+
* would throw away the only reason to stream. The events are `delta`
|
|
89
|
+
* (`{text}`), `done` (`VisitorTurnDone`) and `error` (`{message}`).
|
|
90
|
+
*
|
|
91
|
+
* **The transcript is not a parameter**, which is the security property this
|
|
92
|
+
* endpoint is built around: the caller is ultimately a browser on a public
|
|
93
|
+
* page, and a client that can supply its own transcript can supply one in
|
|
94
|
+
* which anything has already been established. Only the conversation id and
|
|
95
|
+
* the new message travel.
|
|
96
|
+
*/
|
|
97
|
+
chat(input, options = {}) {
|
|
98
|
+
return (0, http_js_1.request)(this.transport, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
path: "/visitor/chat",
|
|
101
|
+
json: input,
|
|
102
|
+
headers: visitorHeaders(options),
|
|
103
|
+
raw: true,
|
|
104
|
+
signal: options.signal,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Book the call a visitor chose.
|
|
109
|
+
*
|
|
110
|
+
* `startAt` must be a time the turn's own slot lookup returned - an arbitrary
|
|
111
|
+
* instant is refused. That, plus a name and phone collected in real fields,
|
|
112
|
+
* is what makes it impossible to book an appointment at a time nobody offered
|
|
113
|
+
* or with a phone number a model parsed out of prose.
|
|
114
|
+
*
|
|
115
|
+
* A conversation books at most one appointment; a second call answers `409`.
|
|
116
|
+
*/
|
|
117
|
+
book(input, options = {}) {
|
|
118
|
+
return (0, http_js_1.request)(this.transport, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
path: "/visitor/book",
|
|
121
|
+
headers: visitorHeaders(options),
|
|
122
|
+
signal: options.signal,
|
|
123
|
+
json: {
|
|
124
|
+
...input,
|
|
125
|
+
startAt: input.startAt instanceof Date ? input.startAt.toISOString() : input.startAt,
|
|
126
|
+
email: input.email ?? null,
|
|
127
|
+
notes: input.notes ?? null,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
exports.VisitorResource = VisitorResource;
|
|
133
|
+
/**
|
|
134
|
+
* A subscribing workspace's **public** knowledge base - what their website chat
|
|
135
|
+
* may tell strangers.
|
|
136
|
+
*
|
|
137
|
+
* ## It is deliberately not their Context Index
|
|
138
|
+
*
|
|
139
|
+
* `ai.chat` answers from a workspace's Drive-fed corpus, for a signed-in member
|
|
140
|
+
* of that workspace. This is a separate store that holds only what somebody
|
|
141
|
+
* explicitly published, and the separation is the reason a publishable widget
|
|
142
|
+
* credential is safe to publish: everything the widget can reach was already
|
|
143
|
+
* meant for the public.
|
|
144
|
+
*
|
|
145
|
+
* Do not try to fill it by uploading to Drive. Nothing reaches this store as a
|
|
146
|
+
* side effect of anything else - that is the invariant, not an inconvenience.
|
|
147
|
+
*
|
|
148
|
+
* ## Publishing is its own call
|
|
149
|
+
*
|
|
150
|
+
* `save` always writes a draft. `publish` is what makes text readable by
|
|
151
|
+
* strangers, so no edit can do it by accident, and `unpublish` can fail - which
|
|
152
|
+
* means the item is **still live**, not that the call was pointless.
|
|
153
|
+
*/
|
|
154
|
+
class VisitorKnowledgeResource {
|
|
155
|
+
transport;
|
|
156
|
+
constructor(transport) {
|
|
157
|
+
this.transport = transport;
|
|
158
|
+
}
|
|
159
|
+
/** Everything, drafts included - the list a customer audits. */
|
|
160
|
+
list(workspaceId) {
|
|
161
|
+
return (0, http_js_1.request)(this.transport, {
|
|
162
|
+
method: "GET",
|
|
163
|
+
path: "/visitor/knowledge",
|
|
164
|
+
query: { workspaceId },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Create or edit a draft.
|
|
169
|
+
*
|
|
170
|
+
* A body over the cap is **refused rather than truncated** - split a long page
|
|
171
|
+
* into sections, which is also what makes retrieval work.
|
|
172
|
+
*/
|
|
173
|
+
async save(input) {
|
|
174
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
path: "/visitor/knowledge",
|
|
177
|
+
json: {
|
|
178
|
+
...input,
|
|
179
|
+
itemId: input.itemId ?? null,
|
|
180
|
+
sourceRef: input.sourceRef ?? null,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return result.item;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Make one item readable by strangers.
|
|
187
|
+
*
|
|
188
|
+
* May be slow: the publish that outgrows the prompt provisions a retrieval
|
|
189
|
+
* corpus and indexes everything published so far. An indexing failure still
|
|
190
|
+
* publishes and comes back with `indexState: "failed"` and `indexError` set,
|
|
191
|
+
* so a provider hiccup never makes your button look broken.
|
|
192
|
+
*/
|
|
193
|
+
async publish(input) {
|
|
194
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
path: "/visitor/knowledge/publish",
|
|
197
|
+
json: input,
|
|
198
|
+
});
|
|
199
|
+
return result.item;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Take one item out of what strangers can read.
|
|
203
|
+
*
|
|
204
|
+
* **A failure means it is still live.** The retrievable copy is removed before
|
|
205
|
+
* the record is flipped, so a provider that refuses the delete aborts rather
|
|
206
|
+
* than leaving an item your screen shows as withdrawn that the widget goes on
|
|
207
|
+
* quoting. Surface the error and let them retry; do not grey the row out
|
|
208
|
+
* optimistically.
|
|
209
|
+
*/
|
|
210
|
+
async unpublish(input) {
|
|
211
|
+
const result = await (0, http_js_1.request)(this.transport, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
path: "/visitor/knowledge/unpublish",
|
|
214
|
+
json: input,
|
|
215
|
+
});
|
|
216
|
+
return result.item;
|
|
217
|
+
}
|
|
218
|
+
/** Remove it entirely. A published item is unpublished first. */
|
|
219
|
+
async delete(input) {
|
|
220
|
+
await (0, http_js_1.request)(this.transport, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
path: "/visitor/knowledge/delete",
|
|
223
|
+
json: input,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
exports.VisitorKnowledgeResource = VisitorKnowledgeResource;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
4
|
import { Ban, AlertTriangle, Building2, Clock, CreditCard, ExternalLink, User, X } from "lucide-react";
|
|
5
5
|
import { Button, Card } from "@heroui/react";
|
|
6
6
|
import { blockingBlockers } from "../../helpers.js";
|
|
@@ -36,7 +36,19 @@ function formatDate(iso) {
|
|
|
36
36
|
*/
|
|
37
37
|
export function useConnectAccessGate() {
|
|
38
38
|
const { session, isLoading: sessionLoading } = useConnect();
|
|
39
|
-
const { data, isLoading: entitlementsLoading } = useConnectEntitlements();
|
|
39
|
+
const { data, isLoading: entitlementsLoading, refetch } = useConnectEntitlements();
|
|
40
|
+
return { ...decideAccess(session, sessionLoading, data, entitlementsLoading), refresh: refetch };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The priority ordering, with the reads lifted out.
|
|
44
|
+
*
|
|
45
|
+
* Split from the hook so the twelve verdicts stay one `return` each and none
|
|
46
|
+
* of them has to carry `refresh` - a field that is the same function in every
|
|
47
|
+
* branch and says nothing about which branch it is. Pure, like
|
|
48
|
+
* `rbac/policy.ts` and `evaluateSignInRisk`, and for the same reason: a
|
|
49
|
+
* decision spread across its inputs cannot be tested.
|
|
50
|
+
*/
|
|
51
|
+
function decideAccess(session, sessionLoading, data, entitlementsLoading) {
|
|
40
52
|
if (sessionLoading) {
|
|
41
53
|
return { state: "loading", blocker: null, paymentRetry: null };
|
|
42
54
|
}
|
|
@@ -147,6 +159,49 @@ export function useConnectAccessGate() {
|
|
|
147
159
|
*/
|
|
148
160
|
export function ConnectAccessGate({ children, fallback, loading, className }) {
|
|
149
161
|
const gate = useConnectAccessGate();
|
|
162
|
+
const isBlocked = gate.state !== "loading" &&
|
|
163
|
+
gate.state !== "signed_out" &&
|
|
164
|
+
gate.state !== "clear" &&
|
|
165
|
+
gate.state !== "payment_retry";
|
|
166
|
+
/**
|
|
167
|
+
* A blocked gate re-checks when the tab comes back.
|
|
168
|
+
*
|
|
169
|
+
* Every blocker this component draws is resolved **somewhere else** - on
|
|
170
|
+
* Connect, in another tab, by an admin, on a phone - and none of that
|
|
171
|
+
* reaches this tab. The resolve button used to open Connect in a new tab,
|
|
172
|
+
* so the overwhelmingly common path was: subscribe over there, switch back,
|
|
173
|
+
* and find the same "Choose a plan to continue" card, with a reload the
|
|
174
|
+
* only way out. It read as the purchase not having worked.
|
|
175
|
+
*
|
|
176
|
+
* The button is a same-tab navigation now (see `GateCard`), which handles
|
|
177
|
+
* the common case on its own. This covers the rest: a second tab, a
|
|
178
|
+
* colleague with the admin rights, a payment settled on a phone.
|
|
179
|
+
*
|
|
180
|
+
* Only while blocked, deliberately. A `clear` gate re-reading entitlements
|
|
181
|
+
* on every tab focus would put a request on the critical path of
|
|
182
|
+
* alt-tabbing back into an application that is working fine, for every
|
|
183
|
+
* workspace, all day. Nothing is gained by it: the gate has nothing to
|
|
184
|
+
* show, and losing access mid-session is what the server-side check before
|
|
185
|
+
* each action is for.
|
|
186
|
+
*/
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!isBlocked || typeof document === "undefined")
|
|
189
|
+
return;
|
|
190
|
+
const recheck = () => {
|
|
191
|
+
if (document.visibilityState === "visible")
|
|
192
|
+
gate.refresh();
|
|
193
|
+
};
|
|
194
|
+
document.addEventListener("visibilitychange", recheck);
|
|
195
|
+
window.addEventListener("focus", recheck);
|
|
196
|
+
return () => {
|
|
197
|
+
document.removeEventListener("visibilitychange", recheck);
|
|
198
|
+
window.removeEventListener("focus", recheck);
|
|
199
|
+
};
|
|
200
|
+
// `gate.refresh` is stable per render of the provider and re-subscribing
|
|
201
|
+
// on it would churn the listeners; `isBlocked` is the only thing that
|
|
202
|
+
// should add or remove them.
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
}, [isBlocked]);
|
|
150
205
|
// `children`, unless the host explicitly asked for a placeholder. See the
|
|
151
206
|
// doc comment - this is the branch that decides whether a partner's page
|
|
152
207
|
// can stream.
|
|
@@ -215,9 +270,27 @@ function AccessGateFallback({ state, blocker, className, }) {
|
|
|
215
270
|
* so "see plans" landing back on the same blocked screen never gets anyone
|
|
216
271
|
* to a choice they can act on. Sending them to Connect's own subscribe page
|
|
217
272
|
* always resolves.
|
|
273
|
+
*
|
|
274
|
+
* ### Same tab, and it carries the way back
|
|
275
|
+
*
|
|
276
|
+
* It used to `window.open(resolveUrl, "_blank")`. A new tab is the wrong
|
|
277
|
+
* shape for a step you have to finish before you can continue: the customer
|
|
278
|
+
* subscribes over there, is left on Connect's billing page with nothing
|
|
279
|
+
* pointing home, and the tab they started in is still showing this card. The
|
|
280
|
+
* most common ending was two open tabs and the belief that the payment had
|
|
281
|
+
* not worked.
|
|
282
|
+
*
|
|
283
|
+
* So: a same-tab navigation carrying `?next=` back to the page they are
|
|
284
|
+
* standing on, which Connect honours after a subscribe once the origin
|
|
285
|
+
* matches one registered for this application's client
|
|
286
|
+
* (`resolveApplicationReturnUrl`). An unregistered origin costs the return
|
|
287
|
+
* trip and nothing else - they still reach Connect, still resolve the
|
|
288
|
+
* blocker, and `ConnectAccessGate`'s focus re-check catches them on the way
|
|
289
|
+
* back. Partners should register their app's origin; the fallback is a
|
|
290
|
+
* degraded path, not a broken one.
|
|
218
291
|
*/
|
|
219
292
|
function GateCard({ icon: Icon, iconClassName, title, message, actionLabel, resolveUrl, className, }) {
|
|
220
|
-
return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), _jsx(Card.Title, { children: title })] }), _jsx(Card.Description, { children: message })] }), resolveUrl && (_jsx(Card.Footer, { children: _jsxs(Button, { variant: "primary", onPress: () => window.
|
|
293
|
+
return (_jsx(MotionProvider, { children: _jsx(m.div, { initial: { opacity: 0, y: -6 }, animate: { opacity: 1, y: 0 }, transition: ENTER, children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Icon, { className: `size-5 shrink-0 ${iconClassName}`, "aria-hidden": "true" }), _jsx(Card.Title, { children: title })] }), _jsx(Card.Description, { children: message })] }), resolveUrl && (_jsx(Card.Footer, { children: _jsxs(Button, { variant: "primary", onPress: () => window.location.assign(withReturnTo(resolveUrl)), children: [actionLabel ?? "Resolve on Connect", _jsx(ExternalLink, { className: "size-3.5" })] }) }))] }) }) }));
|
|
221
294
|
}
|
|
222
295
|
/**
|
|
223
296
|
* The skippable payment-due warning. Dismissal is scoped to this specific
|
|
@@ -236,3 +309,26 @@ function PaymentRetryBanner({ blocker, className, }) {
|
|
|
236
309
|
window.localStorage.setItem(dismissKey, "1");
|
|
237
310
|
}, children: _jsx(X, { className: "size-4" }) })] })] }) }));
|
|
238
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* `resolveUrl` with `?next=` pointing at the page the customer is standing
|
|
314
|
+
* on, so Connect can hand them back once the blocker is gone.
|
|
315
|
+
*
|
|
316
|
+
* Returns the URL untouched if anything is off - no `window`, an unparseable
|
|
317
|
+
* `resolveUrl`, a `next` already set by the server. Getting back is a
|
|
318
|
+
* courtesy on top of resolving the blocker, and it must never be the reason
|
|
319
|
+
* somebody cannot reach the screen that resolves it.
|
|
320
|
+
*/
|
|
321
|
+
function withReturnTo(resolveUrl) {
|
|
322
|
+
if (typeof window === "undefined")
|
|
323
|
+
return resolveUrl;
|
|
324
|
+
try {
|
|
325
|
+
const url = new URL(resolveUrl);
|
|
326
|
+
if (url.searchParams.has("next"))
|
|
327
|
+
return resolveUrl;
|
|
328
|
+
url.searchParams.set("next", window.location.href);
|
|
329
|
+
return url.toString();
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return resolveUrl;
|
|
333
|
+
}
|
|
334
|
+
}
|