@cosmoops/connect-sdk 0.33.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 +111 -0
- package/README.md +17 -5
- package/dist/cjs/client/components/access-gate.js +98 -2
- package/dist/cjs/client/provider.js +5 -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 +1 -1
- package/dist/cjs/server/resources/billing.js +5 -0
- package/dist/cjs/server/resources/entitlements.js +47 -0
- package/dist/esm/client/components/access-gate.js +99 -3
- package/dist/esm/client/provider.js +5 -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 +1 -1
- package/dist/esm/server/resources/billing.js +5 -0
- package/dist/esm/server/resources/entitlements.js +47 -0
- package/dist/types/client/components/access-gate.d.ts +9 -0
- package/dist/types/client/provider.d.ts +155 -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 +1 -1
- package/dist/types/server/resources/billing.d.ts +4 -0
- package/dist/types/server/resources/entitlements.d.ts +40 -1
- package/dist/types/types.d.ts +149 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -483,7 +483,7 @@ export function useConnectSessions() {
|
|
|
483
483
|
*/
|
|
484
484
|
export function useConnectResource(path, options) {
|
|
485
485
|
const requiresSession = options?.requiresSession ?? true;
|
|
486
|
-
const { fetchResource, session, isLoading: sessionLoading } = useConnect();
|
|
486
|
+
const { fetchResource, invalidateResource, session, isLoading: sessionLoading } = useConnect();
|
|
487
487
|
// Stamped with the `path` it belongs to, so switching paths reports loading
|
|
488
488
|
// without a synchronous `setIsLoading(true)` - and, more usefully, so a
|
|
489
489
|
// resolved response for a previous path can never be read as this one's.
|
|
@@ -523,6 +523,9 @@ export function useConnectResource(path, options) {
|
|
|
523
523
|
// should render its signed-out state, not skeletons forever.
|
|
524
524
|
isLoading: !signedOut && !loaded,
|
|
525
525
|
error: loaded?.error ?? null,
|
|
526
|
-
refetch: () =>
|
|
526
|
+
refetch: () => {
|
|
527
|
+
invalidateResource(path);
|
|
528
|
+
setVersion((v) => v + 1);
|
|
529
|
+
},
|
|
527
530
|
};
|
|
528
531
|
}
|
|
@@ -161,12 +161,14 @@ export function buildCancelUrl({ appUrl, subscriptionId }) {
|
|
|
161
161
|
* prefix that has never served this page. Building the other one is what
|
|
162
162
|
* sent every first-time subscriber from a partner's pricing page to a 404.
|
|
163
163
|
*/
|
|
164
|
-
export function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, }) {
|
|
164
|
+
export function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, next, }) {
|
|
165
165
|
const path = subscriptionId
|
|
166
166
|
? `/billing/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan/${encodeURIComponent(planId)}`
|
|
167
167
|
: `/apps/${encodeURIComponent(applicationId)}/subscribe/${encodeURIComponent(planId)}`;
|
|
168
168
|
const url = new URL(path, appUrl);
|
|
169
169
|
if (coupon?.trim())
|
|
170
170
|
url.searchParams.set("coupon", coupon.trim());
|
|
171
|
+
if (next?.trim())
|
|
172
|
+
url.searchParams.set("next", next.trim());
|
|
171
173
|
return url.toString();
|
|
172
174
|
}
|
package/dist/esm/helpers.js
CHANGED
|
@@ -24,3 +24,46 @@ export function blockingBlockers(readiness) {
|
|
|
24
24
|
export function normalizePlanFeatures(features) {
|
|
25
25
|
return (features ?? []).map((feature) => typeof feature === "string" ? { key: feature, name: feature, description: "" } : feature);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* What a key the plan carries no answer for reads as - see
|
|
29
|
+
* `SubscribedPlan.limits`, which is sparse.
|
|
30
|
+
*
|
|
31
|
+
* `"none"` rather than `"unlimited"`: a plan that was never asked about a
|
|
32
|
+
* limit has not sold it, and defaulting the other way would hand an unbounded
|
|
33
|
+
* allowance to every tier that predates the question.
|
|
34
|
+
*/
|
|
35
|
+
export const NO_PLAN_LIMIT = { model: "none", value: null };
|
|
36
|
+
/**
|
|
37
|
+
* One plan's answer for one limit key, with the sparse-map default applied.
|
|
38
|
+
*
|
|
39
|
+
* Indexing `plan.limits` directly is what makes a missing key `undefined`,
|
|
40
|
+
* which then reads as unlimited through the first `?.` somebody writes. This
|
|
41
|
+
* returns a `PlanLimit` either way.
|
|
42
|
+
*/
|
|
43
|
+
export function planLimitFor(limits, key) {
|
|
44
|
+
return limits?.[key] ?? NO_PLAN_LIMIT;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether `additional` more of the resource fit under this limit, given how
|
|
48
|
+
* many the subscriber already has. The comparison, written once.
|
|
49
|
+
*
|
|
50
|
+
* Every way of writing it at a call site is wrong in a different direction:
|
|
51
|
+
* `count < limit.value` is off by one as soon as you add more than one row,
|
|
52
|
+
* `if (!limit.value)` refuses the unlimited tier, and
|
|
53
|
+
* `limit.value ?? Infinity` grants unlimited to a plan that allows none. This
|
|
54
|
+
* is the same implementation Connect's own server uses to validate a plan.
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* const limit = await connect.entitlements.getLimit(workspaceId, "sites");
|
|
58
|
+
* if (!planLimitAllows(limit, await countSites(workspaceId))) {
|
|
59
|
+
* throw new Error("Your plan's site limit is reached.");
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function planLimitAllows(limit, currentCount, additional = 1) {
|
|
64
|
+
if (limit.model === "unlimited")
|
|
65
|
+
return true;
|
|
66
|
+
if (limit.model === "none")
|
|
67
|
+
return false;
|
|
68
|
+
return currentCount + additional <= (limit.value ?? 0);
|
|
69
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -15,5 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
export { ConnectError, isConnectError } from "./errors.js";
|
|
17
17
|
export { blockingBlockers, normalizePlanFeatures } from "./helpers.js";
|
|
18
|
+
export { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "./helpers.js";
|
|
18
19
|
export { arrayRemove, arrayUnion, deleteField, documentExists, increment, serverTimestamp, storeIndexFor, storeIndexSignature, unchangedSince, } from "./store.js";
|
|
19
20
|
export { describeQueryRejection, storeIndexCovers } from "./store-schema.js";
|
package/dist/esm/next/index.js
CHANGED
|
@@ -35,11 +35,18 @@ import { ConnectError } from "../errors.js";
|
|
|
35
35
|
* | --- | --- | --- |
|
|
36
36
|
* | `CONNECT_API_KEY` | yes | server-side credential |
|
|
37
37
|
* | `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
38
|
-
* | `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
39
|
-
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
38
|
+
* | `CONNECT_APP_URL` | no | non-production Connect instance; unset means production |
|
|
39
|
+
* | `CONNECT_SESSION_COOKIE` | no | session cookie name; unset means `connect_session` |
|
|
40
|
+
*
|
|
41
|
+
* `CONNECT_APP_URL` is server-side only and does not reach the browser:
|
|
42
|
+
* `ConnectProvider` defaults `appUrl` to the production origin independently,
|
|
43
|
+
* so an app pointed at another instance must pass that prop as well or its
|
|
44
|
+
* portal links and its browser-built authorize URL still go to production.
|
|
40
45
|
*
|
|
41
46
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
42
|
-
* application's Connect client section. That is the only step this cannot do
|
|
47
|
+
* application's Connect client section. That is the only step this cannot do
|
|
48
|
+
* for you, and Connect matches it exactly - every origin you run from needs
|
|
49
|
+
* its own entry, localhost included.
|
|
43
50
|
*
|
|
44
51
|
* No `next.config.ts` changes needed either, including `transpilePackages`:
|
|
45
52
|
* `dist/` is already ES2022 with `"use client"` hoisted to the top of every
|
|
@@ -14,6 +14,10 @@ export class BillingResource {
|
|
|
14
14
|
* independently of the instance it talks to. Without the coercion a partner
|
|
15
15
|
* pointed at a Connect that has not upgraded yet renders a feature list of
|
|
16
16
|
* blanks - which is exactly what happened. See `normalizePlanFeatures`.
|
|
17
|
+
*
|
|
18
|
+
* `limits` is defaulted for the same reason, one version later: an instance
|
|
19
|
+
* that predates limits sends no such field, and a pricing grid mapping over
|
|
20
|
+
* `plan.limits` would throw rather than simply show no limit rows.
|
|
17
21
|
*/
|
|
18
22
|
async listPlans() {
|
|
19
23
|
const response = await request(this.transport, { method: "GET", path: "/plans" });
|
|
@@ -21,6 +25,7 @@ export class BillingResource {
|
|
|
21
25
|
plans: (response.plans ?? []).map((plan) => ({
|
|
22
26
|
...plan,
|
|
23
27
|
features: normalizePlanFeatures(plan.features),
|
|
28
|
+
limits: plan.limits ?? [],
|
|
24
29
|
})),
|
|
25
30
|
};
|
|
26
31
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pathSegment, request } from "../http.js";
|
|
2
|
+
import { NO_PLAN_LIMIT, planLimitAllows, planLimitFor } from "../../helpers.js";
|
|
2
3
|
/**
|
|
3
4
|
* The two checks worth making on every request: may this workspace be here at
|
|
4
5
|
* all, and what is it paying for.
|
|
@@ -42,6 +43,52 @@ export class EntitlementsResource {
|
|
|
42
43
|
const result = await this.getSubscription(workspaceId);
|
|
43
44
|
return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* How much of a metered resource this workspace may have - the numeric
|
|
48
|
+
* counterpart of `hasFeature`, for the resources your application counts
|
|
49
|
+
* itself (sites, projects, pipelines).
|
|
50
|
+
*
|
|
51
|
+
* **This is the effective ceiling, not the plan's own number.** Connect has
|
|
52
|
+
* already folded in whatever the add-ons on the subscription raise it by, so
|
|
53
|
+
* "Starter includes 1 site, they bought two 5-site packs" arrives here as
|
|
54
|
+
* `{ model: "limited", value: 11 }`. You never join the plan against the
|
|
55
|
+
* add-on list yourself, and a customer who buys another pack needs no change
|
|
56
|
+
* on your side.
|
|
57
|
+
*
|
|
58
|
+
* Returns `{ model: "none" }` rather than throwing when there is no active
|
|
59
|
+
* subscription, when the plan carries no answer for this key, and when the
|
|
60
|
+
* Connect instance predates limits entirely - the same reasoning
|
|
61
|
+
* `hasFeature` gives for answering `false`: "not entitled" is the same answer
|
|
62
|
+
* either way, and a limit check should not need a try/catch around it.
|
|
63
|
+
*
|
|
64
|
+
* Connect does not enforce this. It cannot see what you are counting, so it
|
|
65
|
+
* hands you the number and your code refuses the next row - `canAdd` is that
|
|
66
|
+
* comparison if you already have the count.
|
|
67
|
+
*/
|
|
68
|
+
async getLimit(workspaceId, key) {
|
|
69
|
+
const result = await this.getSubscription(workspaceId);
|
|
70
|
+
if (!result.hasActiveSubscription)
|
|
71
|
+
return NO_PLAN_LIMIT;
|
|
72
|
+
return planLimitFor(result.plan?.limits, key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether this workspace may add `additional` more of a limited resource,
|
|
76
|
+
* given how many it already has.
|
|
77
|
+
*
|
|
78
|
+
* You supply the count, because only your application can produce it. This
|
|
79
|
+
* is `getLimit` plus `planLimitAllows`, which exists so the comparison is
|
|
80
|
+
* not rewritten per call site - see that helper for the three ways of
|
|
81
|
+
* getting it wrong.
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
|
|
85
|
+
* return refuse("Your plan's site limit is reached.");
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
async canAdd(workspaceId, key, currentCount, additional = 1) {
|
|
90
|
+
return planLimitAllows(await this.getLimit(workspaceId, key), currentCount, additional);
|
|
91
|
+
}
|
|
45
92
|
/**
|
|
46
93
|
* One page of the workspaces subscribed to your application, newest first.
|
|
47
94
|
*
|
|
@@ -23,6 +23,15 @@ export type ConnectAccessGateState = "loading"
|
|
|
23
23
|
| "clear";
|
|
24
24
|
export interface ConnectAccessGateResult {
|
|
25
25
|
state: ConnectAccessGateState;
|
|
26
|
+
/**
|
|
27
|
+
* Re-reads entitlements, evicting the cached answer first.
|
|
28
|
+
*
|
|
29
|
+
* Exposed because a blocker is almost always resolved **somewhere this tab
|
|
30
|
+
* cannot see** - on Connect, in another tab or on a phone - and nothing
|
|
31
|
+
* about that reaches this one. The gate wires it to the tab regaining
|
|
32
|
+
* focus; call it yourself if your app learns of a change some other way.
|
|
33
|
+
*/
|
|
34
|
+
refresh(): void;
|
|
26
35
|
/** The blocker driving a hard-block state (`suspended`/`no_subscription`/`onboarding_pending`/`blocked`). `null` otherwise. */
|
|
27
36
|
blocker: ReadinessBlocker | null;
|
|
28
37
|
/** The `payment_retry_pending` blocker, only when `state === "payment_retry"`. Carries `graceEndsAt`. */
|
|
@@ -196,7 +196,21 @@ export interface ConnectProviderProps {
|
|
|
196
196
|
* not a Connect URL - it is your backend that exchanges the code.
|
|
197
197
|
*/
|
|
198
198
|
redirectUri?: string;
|
|
199
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Override for non-production Connect instances. Defaults to
|
|
201
|
+
* `https://connect.cosmoops.com`.
|
|
202
|
+
*
|
|
203
|
+
* The twin of `CONNECT_APP_URL`, and *not* derived from it - that variable
|
|
204
|
+
* is read on your server and never reaches the browser. This value is where
|
|
205
|
+
* the user menu's Profile, Billing and Invoices destinations open, where
|
|
206
|
+
* `ConnectAppsGrid` sends an unsubscribed application, where checkout
|
|
207
|
+
* opens, and - if you pass `redirectUri` and so build the authorize URL
|
|
208
|
+
* here rather than in your own `/signin` route - where sign-in goes. Point
|
|
209
|
+
* the server somewhere else and leave this alone and the two halves
|
|
210
|
+
* disagree, so pass both or neither. Your root layout is a Server
|
|
211
|
+
* Component, so `appUrl={process.env.CONNECT_APP_URL}` is enough and needs
|
|
212
|
+
* no browser-visible variable.
|
|
213
|
+
*/
|
|
200
214
|
appUrl?: string;
|
|
201
215
|
/**
|
|
202
216
|
* Replace the `fetch` used to reach your bridge. The browser-side twin of
|
|
@@ -327,11 +341,20 @@ export declare function useConnectEntitlements(): {
|
|
|
327
341
|
isLoading: boolean;
|
|
328
342
|
error: string | null;
|
|
329
343
|
/**
|
|
330
|
-
* Forces a fresh read -
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
344
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
345
|
+
* read.
|
|
346
|
+
*
|
|
347
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
348
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
349
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
350
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
351
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
352
|
+
* the same settled promise and reported the same stale answer forever -
|
|
353
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
354
|
+
* whose workspace had subscribed in a different one.
|
|
355
|
+
*
|
|
356
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
357
|
+
* entry that is already gone is a no-op.
|
|
335
358
|
*/
|
|
336
359
|
refetch(): void;
|
|
337
360
|
};
|
|
@@ -347,11 +370,20 @@ export declare function useConnectAddonUsage(): {
|
|
|
347
370
|
isLoading: boolean;
|
|
348
371
|
error: string | null;
|
|
349
372
|
/**
|
|
350
|
-
* Forces a fresh read -
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
373
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
374
|
+
* read.
|
|
375
|
+
*
|
|
376
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
377
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
378
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
379
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
380
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
381
|
+
* the same settled promise and reported the same stale answer forever -
|
|
382
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
383
|
+
* whose workspace had subscribed in a different one.
|
|
384
|
+
*
|
|
385
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
386
|
+
* entry that is already gone is a no-op.
|
|
355
387
|
*/
|
|
356
388
|
refetch(): void;
|
|
357
389
|
};
|
|
@@ -361,11 +393,20 @@ export declare function useConnectPlans(): {
|
|
|
361
393
|
isLoading: boolean;
|
|
362
394
|
error: string | null;
|
|
363
395
|
/**
|
|
364
|
-
* Forces a fresh read -
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
396
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
397
|
+
* read.
|
|
398
|
+
*
|
|
399
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
400
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
401
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
402
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
403
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
404
|
+
* the same settled promise and reported the same stale answer forever -
|
|
405
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
406
|
+
* whose workspace had subscribed in a different one.
|
|
407
|
+
*
|
|
408
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
409
|
+
* entry that is already gone is a no-op.
|
|
369
410
|
*/
|
|
370
411
|
refetch(): void;
|
|
371
412
|
};
|
|
@@ -384,11 +425,20 @@ export declare function useConnectCompanyProfile(): {
|
|
|
384
425
|
isLoading: boolean;
|
|
385
426
|
error: string | null;
|
|
386
427
|
/**
|
|
387
|
-
* Forces a fresh read -
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
428
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
429
|
+
* read.
|
|
430
|
+
*
|
|
431
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
432
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
433
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
434
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
435
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
436
|
+
* the same settled promise and reported the same stale answer forever -
|
|
437
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
438
|
+
* whose workspace had subscribed in a different one.
|
|
439
|
+
*
|
|
440
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
441
|
+
* entry that is already gone is a no-op.
|
|
392
442
|
*/
|
|
393
443
|
refetch(): void;
|
|
394
444
|
};
|
|
@@ -400,11 +450,20 @@ export declare function useConnectApps(): {
|
|
|
400
450
|
isLoading: boolean;
|
|
401
451
|
error: string | null;
|
|
402
452
|
/**
|
|
403
|
-
* Forces a fresh read -
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
453
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
454
|
+
* read.
|
|
455
|
+
*
|
|
456
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
457
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
458
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
459
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
460
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
461
|
+
* the same settled promise and reported the same stale answer forever -
|
|
462
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
463
|
+
* whose workspace had subscribed in a different one.
|
|
464
|
+
*
|
|
465
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
466
|
+
* entry that is already gone is a no-op.
|
|
408
467
|
*/
|
|
409
468
|
refetch(): void;
|
|
410
469
|
};
|
|
@@ -423,11 +482,20 @@ export declare function useConnectWorkspaces(): {
|
|
|
423
482
|
isLoading: boolean;
|
|
424
483
|
error: string | null;
|
|
425
484
|
/**
|
|
426
|
-
* Forces a fresh read -
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
485
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
486
|
+
* read.
|
|
487
|
+
*
|
|
488
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
489
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
490
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
491
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
492
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
493
|
+
* the same settled promise and reported the same stale answer forever -
|
|
494
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
495
|
+
* whose workspace had subscribed in a different one.
|
|
496
|
+
*
|
|
497
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
498
|
+
* entry that is already gone is a no-op.
|
|
431
499
|
*/
|
|
432
500
|
refetch(): void;
|
|
433
501
|
};
|
|
@@ -442,11 +510,20 @@ export declare function useConnectWorkspaceMembers(): {
|
|
|
442
510
|
isLoading: boolean;
|
|
443
511
|
error: string | null;
|
|
444
512
|
/**
|
|
445
|
-
* Forces a fresh read -
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
513
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
514
|
+
* read.
|
|
515
|
+
*
|
|
516
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
517
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
518
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
519
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
520
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
521
|
+
* the same settled promise and reported the same stale answer forever -
|
|
522
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
523
|
+
* whose workspace had subscribed in a different one.
|
|
524
|
+
*
|
|
525
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
526
|
+
* entry that is already gone is a no-op.
|
|
450
527
|
*/
|
|
451
528
|
refetch(): void;
|
|
452
529
|
};
|
|
@@ -456,11 +533,20 @@ export declare function useConnectIntegrations(): {
|
|
|
456
533
|
isLoading: boolean;
|
|
457
534
|
error: string | null;
|
|
458
535
|
/**
|
|
459
|
-
* Forces a fresh read -
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
536
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
537
|
+
* read.
|
|
538
|
+
*
|
|
539
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
540
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
541
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
542
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
543
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
544
|
+
* the same settled promise and reported the same stale answer forever -
|
|
545
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
546
|
+
* whose workspace had subscribed in a different one.
|
|
547
|
+
*
|
|
548
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
549
|
+
* entry that is already gone is a no-op.
|
|
464
550
|
*/
|
|
465
551
|
refetch(): void;
|
|
466
552
|
};
|
|
@@ -476,11 +562,20 @@ export declare function useConnectSessions(): {
|
|
|
476
562
|
isLoading: boolean;
|
|
477
563
|
error: string | null;
|
|
478
564
|
/**
|
|
479
|
-
* Forces a fresh read -
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
565
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
566
|
+
* read.
|
|
567
|
+
*
|
|
568
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
569
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
570
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
571
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
572
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
573
|
+
* the same settled promise and reported the same stale answer forever -
|
|
574
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
575
|
+
* whose workspace had subscribed in a different one.
|
|
576
|
+
*
|
|
577
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
578
|
+
* entry that is already gone is a no-op.
|
|
484
579
|
*/
|
|
485
580
|
refetch(): void;
|
|
486
581
|
};
|
|
@@ -512,11 +607,20 @@ export declare function useConnectResource<T>(path: string, options?: Readonly<U
|
|
|
512
607
|
isLoading: boolean;
|
|
513
608
|
error: string | null;
|
|
514
609
|
/**
|
|
515
|
-
* Forces a fresh read -
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
610
|
+
* Forces a fresh read - evicts this path's cache entry and re-runs the
|
|
611
|
+
* read.
|
|
612
|
+
*
|
|
613
|
+
* It used only to re-trigger the effect, on the assumption that the caller
|
|
614
|
+
* had just evicted the entry itself through `mutateResource`. That made it
|
|
615
|
+
* useless for the other reason to refetch: **something changed somewhere
|
|
616
|
+
* this tab cannot see.** The provider's cache is a `Map` of resolved
|
|
617
|
+
* promises with no expiry, so a `refetch()` without the eviction re-read
|
|
618
|
+
* the same settled promise and reported the same stale answer forever -
|
|
619
|
+
* which is what left `<ConnectAccessGate>` showing "Choose a plan" in a tab
|
|
620
|
+
* whose workspace had subscribed in a different one.
|
|
621
|
+
*
|
|
622
|
+
* Still safe after a `mutateResource` against the same path: evicting an
|
|
623
|
+
* entry that is already gone is a no-op.
|
|
520
624
|
*/
|
|
521
625
|
refetch(): void;
|
|
522
626
|
};
|