@lukoweb/apitogo-plugin-dev-portal-billing 0.2.53 → 0.2.56
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/dist/{AccountPlansPage-DvRFe3di.mjs → AccountPlansPage-BuM7fe82.mjs} +14 -10
- package/dist/{CheckoutPage-CwSElFUk.mjs → CheckoutPage-BSiADcI6.mjs} +32 -15
- package/dist/{ContactSalesPage-q7Dxxtx6.mjs → ContactSalesPage-BqqDmorF.mjs} +4 -11
- package/dist/{CredentialsPage-B2jPo6Go.mjs → CredentialsPage-DjNvBBLb.mjs} +28 -19
- package/dist/{PricingCard-CKdq1oWn.mjs → PricingCard-DFuQEw7Y.mjs} +10 -4
- package/dist/{PricingPage-DnQQfy9L.mjs → PricingPage-SpL81vpd.mjs} +14 -13
- package/dist/SubscriptionsPage-CwsBtNld.mjs +274 -0
- package/dist/{UsagePage-BF4QgrTc.mjs → UsagePage-Dw5Uepg3.mjs} +61 -19
- package/dist/{account-links-D_vS0GJn.mjs → account-links-Drofs-f-.mjs} +1 -1
- package/dist/{billing-state-DbU5Lpoz.mjs → billing-state-DBQR2tad.mjs} +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +25 -23
- package/dist/{key-plan-note-DTWeFhn2.mjs → page-state-BLL2JJBc.mjs} +22 -2
- package/dist/paths-DaVyTIDJ.mjs +14 -0
- package/dist/pending-checkout-DpVaNnTu.mjs +45 -0
- package/dist/problem-details-n20-dy3w.mjs +18 -0
- package/dist/{useApiKeys-CVgXfUop.mjs → useApiKeys-BOY3vIOr.mjs} +3 -9
- package/dist/{useDevPortalApiBaseUrl-BstWzITX.mjs → useDevPortalApiBaseUrl-BiI2jFjU.mjs} +14 -14
- package/dist/{useMySubscriptions-BtxAXA1m.mjs → useMySubscriptions-BYVIIWbe.mjs} +2 -2
- package/dist/{usePlans-DPGFWcVo.mjs → usePlans-HyhIIuJZ.mjs} +1 -1
- package/dist/{useUsage--5sZub3M.mjs → useUsage-B5x9YuKa.mjs} +1 -1
- package/package.json +3 -3
- package/dist/SubscriptionsPage-msO31jv-.mjs +0 -223
- package/dist/paths-ujy9wjVK.mjs +0 -12
- /package/dist/{demo-session-rN2Gj4lM.mjs → demo-session-yK9qEqV-.mjs} +0 -0
- /package/dist/{pricing-config-02m4dyFf.mjs → pricing-config-Bc6eWFUO.mjs} +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-
|
|
2
|
-
import { t as PricingCard } from "./PricingCard-
|
|
1
|
+
import { f as parseManifestPlans, n as useDevPortalPreviewMode, r as useIsDemoSession, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
2
|
+
import { t as PricingCard } from "./PricingCard-DFuQEw7Y.mjs";
|
|
3
3
|
import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
4
|
+
import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
|
|
5
|
+
import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
|
|
6
|
+
import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
|
|
7
|
+
import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
|
|
6
8
|
import { Head } from "@lukoweb/apitogo/components";
|
|
7
9
|
import { CreditCardIcon } from "@lukoweb/apitogo/icons";
|
|
8
10
|
import { useSearchParams } from "@lukoweb/apitogo/router";
|
|
9
|
-
import { useState } from "react";
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
10
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
13
|
import { useMutation, useQueryClient } from "@lukoweb/apitogo/react-query";
|
|
12
14
|
|
|
@@ -43,10 +45,7 @@ function useCancelSubscription() {
|
|
|
43
45
|
credentials: "include",
|
|
44
46
|
body: JSON.stringify({ planSku })
|
|
45
47
|
});
|
|
46
|
-
if (!response.ok)
|
|
47
|
-
const detail = await response.text();
|
|
48
|
-
throw new Error(detail || "Failed to cancel subscription.");
|
|
49
|
-
}
|
|
48
|
+
if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Failed to cancel subscription.");
|
|
50
49
|
return await response.json();
|
|
51
50
|
},
|
|
52
51
|
onSuccess: async () => {
|
|
@@ -72,6 +71,10 @@ const AccountPlansPage = () => {
|
|
|
72
71
|
const cancelMutation = useCancelSubscription();
|
|
73
72
|
const [cancelingSku, setCancelingSku] = useState(null);
|
|
74
73
|
const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (checkoutCancelled) clearPendingCheckout();
|
|
76
|
+
}, [checkoutCancelled]);
|
|
77
|
+
const pendingSku = checkoutCancelled ? void 0 : readPendingCheckout();
|
|
75
78
|
const handleUnsubscribe = async (planSku) => {
|
|
76
79
|
setCancelingSku(planSku);
|
|
77
80
|
try {
|
|
@@ -135,7 +138,8 @@ const AccountPlansPage = () => {
|
|
|
135
138
|
isSubscribed,
|
|
136
139
|
onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
|
|
137
140
|
isCanceling: cancelingSku === plan.sku,
|
|
138
|
-
cancelDisabled: isPreview || isDemoSession
|
|
141
|
+
cancelDisabled: isPreview || isDemoSession,
|
|
142
|
+
isConfirming: !isSubscribed && pendingSku === plan.sku
|
|
139
143
|
}, plan.sku);
|
|
140
144
|
})
|
|
141
145
|
}) : !isLoading && !isError ? /* @__PURE__ */ jsx("div", {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-
|
|
3
|
-
import { t as
|
|
1
|
+
import { n as ALREADY_SUBSCRIBED_PARAM, s as PRICING_PATH, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
|
|
2
|
+
import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
3
|
+
import { n as readProblemDetail, t as isAlreadySubscribedMessage } from "./problem-details-n20-dy3w.mjs";
|
|
4
|
+
import { r as rememberPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
|
|
5
|
+
import { t as FREE_CHECKOUT_MARKER } from "./billing-state-DBQR2tad.mjs";
|
|
4
6
|
import { ShieldIcon } from "@lukoweb/apitogo/icons";
|
|
5
7
|
import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
|
|
6
8
|
import { useEffect } from "react";
|
|
@@ -81,14 +83,23 @@ const CheckoutPage = () => {
|
|
|
81
83
|
credentials: "include",
|
|
82
84
|
body: JSON.stringify({ planSku })
|
|
83
85
|
});
|
|
84
|
-
if (!response.ok)
|
|
85
|
-
const detail = await response.text();
|
|
86
|
-
throw new Error(detail || "Checkout failed.");
|
|
87
|
-
}
|
|
86
|
+
if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Checkout failed.");
|
|
88
87
|
return await response.json();
|
|
89
88
|
}
|
|
90
89
|
});
|
|
91
90
|
const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
|
|
91
|
+
const checkoutUrl = checkoutQuery.data?.checkoutUrl;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (checkoutUrl) rememberPendingCheckout(planSku);
|
|
94
|
+
}, [checkoutUrl, planSku]);
|
|
95
|
+
const alreadySubscribed = checkoutQuery.isError && isAlreadySubscribedMessage(checkoutQuery.error.message);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (alreadySubscribed) navigate(`/account/subscriptions?${ALREADY_SUBSCRIBED_PARAM}=${encodeURIComponent(planSku)}`, { replace: true });
|
|
98
|
+
}, [
|
|
99
|
+
alreadySubscribed,
|
|
100
|
+
navigate,
|
|
101
|
+
planSku
|
|
102
|
+
]);
|
|
92
103
|
useEffect(() => {
|
|
93
104
|
if (freeProvisioned) navigate(`/account/subscriptions?checkout=success&${FREE_CHECKOUT_MARKER}`, { replace: true });
|
|
94
105
|
}, [freeProvisioned, navigate]);
|
|
@@ -146,13 +157,13 @@ const CheckoutPage = () => {
|
|
|
146
157
|
})
|
|
147
158
|
]
|
|
148
159
|
});
|
|
149
|
-
if (freeProvisioned) return null;
|
|
150
|
-
return /* @__PURE__ */
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
if (freeProvisioned || alreadySubscribed) return null;
|
|
161
|
+
if (checkoutQuery.isError) return /* @__PURE__ */ jsxs("div", {
|
|
162
|
+
className: "mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center",
|
|
163
|
+
children: [/* @__PURE__ */ jsx("h1", {
|
|
164
|
+
className: "text-2xl font-semibold tracking-tight",
|
|
165
|
+
children: "Checkout could not start"
|
|
166
|
+
}), /* @__PURE__ */ jsxs(Alert, {
|
|
156
167
|
variant: "destructive",
|
|
157
168
|
children: [/* @__PURE__ */ jsx(AlertDescription, {
|
|
158
169
|
className: "first-letter:uppercase",
|
|
@@ -166,7 +177,13 @@ const CheckoutPage = () => {
|
|
|
166
177
|
children: "Back to plans"
|
|
167
178
|
})
|
|
168
179
|
}) })]
|
|
169
|
-
})
|
|
180
|
+
})]
|
|
181
|
+
});
|
|
182
|
+
return /* @__PURE__ */ jsx(RedirectPage, {
|
|
183
|
+
icon: ShieldIcon,
|
|
184
|
+
title: "Establishing secure checkout…",
|
|
185
|
+
description: "Redirecting you to complete your subscription.",
|
|
186
|
+
url: checkoutQuery.data?.checkoutUrl ?? void 0
|
|
170
187
|
});
|
|
171
188
|
};
|
|
172
189
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-
|
|
1
|
+
import { s as PRICING_PATH } from "./paths-DaVyTIDJ.mjs";
|
|
2
|
+
import { l as isPlanCustomPriced, n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
3
3
|
import "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { n as readProblemDetail } from "./problem-details-n20-dy3w.mjs";
|
|
5
|
+
import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
|
|
5
6
|
import { Link, useSearchParams } from "@lukoweb/apitogo/router";
|
|
6
7
|
import { useState } from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -32,14 +33,6 @@ function useSubmitPlanInquiry() {
|
|
|
32
33
|
if (!response.ok) throw new Error(await readProblemDetail(response) ?? "Your message could not be sent. Please try again later.");
|
|
33
34
|
} });
|
|
34
35
|
}
|
|
35
|
-
async function readProblemDetail(response) {
|
|
36
|
-
try {
|
|
37
|
-
const body = await response.json();
|
|
38
|
-
return (body.errors ? Object.values(body.errors).flat()[0] : void 0) ?? body.detail ?? body.title ?? null;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
36
|
|
|
44
37
|
//#endregion
|
|
45
38
|
//#region src/pages/ContactSalesPage.tsx
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { t as ACCOUNT_PLANS_PATH } from "./paths-
|
|
2
|
-
import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-
|
|
1
|
+
import { t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
|
|
2
|
+
import { n as useDevPortalPreviewMode, r as useIsDemoSession, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
3
3
|
import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
-
import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-
|
|
5
|
-
import { n as useApiKeys } from "./useApiKeys-
|
|
6
|
-
import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-
|
|
7
|
-
import {
|
|
4
|
+
import { r as createDemoCredentials, t as DEMO_GATEWAY_KEY_HEADER } from "./demo-session-yK9qEqV-.mjs";
|
|
5
|
+
import { n as useApiKeys } from "./useApiKeys-BOY3vIOr.mjs";
|
|
6
|
+
import { n as apiReferencePath, r as ApiKeysPanel } from "./account-links-Drofs-f-.mjs";
|
|
7
|
+
import { i as ChoosePlanNotice, r as keyPlanNote, t as apiKeysPageState } from "./page-state-BLL2JJBc.mjs";
|
|
8
8
|
import { Head } from "@lukoweb/apitogo/components";
|
|
9
9
|
import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
|
|
10
10
|
import { Link as Link$1 } from "@lukoweb/apitogo/router";
|
|
@@ -57,18 +57,23 @@ const CredentialsPage = () => {
|
|
|
57
57
|
const auth = useAuth();
|
|
58
58
|
const isPreview = useDevPortalPreviewMode();
|
|
59
59
|
const isDemoSession = useIsDemoSession();
|
|
60
|
-
const { data: credentials
|
|
61
|
-
const { data: apiKeys,
|
|
60
|
+
const { data: credentials } = useCredentials();
|
|
61
|
+
const { data: apiKeys, error: keysError } = useApiKeys();
|
|
62
62
|
const { gatewayBaseUrl: configuredGatewayBaseUrl, links, keyHeader: configuredKeyHeader } = useDevPortalBillingConfig();
|
|
63
63
|
const apiReferenceLink = apiReferencePath(links);
|
|
64
64
|
const gatewayBaseUrl = credentials?.gatewayBaseUrl?.trim() || apiKeys?.gatewayBaseUrl?.trim() || configuredGatewayBaseUrl?.trim() || zudokuConfig.__meta?.gatewayBaseUrl?.trim() || null;
|
|
65
65
|
const header = configuredKeyHeader?.trim() || apiKeys?.header || DEMO_GATEWAY_KEY_HEADER;
|
|
66
66
|
const plans = apiKeys?.plans ?? [];
|
|
67
67
|
const planNote = isDemoSession ? null : keyPlanNote(plans);
|
|
68
|
+
const pageState = apiKeysPageState({
|
|
69
|
+
data: apiKeys,
|
|
70
|
+
error: keysError
|
|
71
|
+
});
|
|
72
|
+
const showsLiveData = auth.isAuthenticated && (isDemoSession || !isPreview);
|
|
68
73
|
return /* @__PURE__ */ jsxs("div", {
|
|
69
74
|
className: "mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8",
|
|
70
75
|
children: [
|
|
71
|
-
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API
|
|
76
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "API keys" }) }),
|
|
72
77
|
/* @__PURE__ */ jsxs("div", {
|
|
73
78
|
className: "mb-8 flex items-start gap-4",
|
|
74
79
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -78,7 +83,7 @@ const CredentialsPage = () => {
|
|
|
78
83
|
className: "space-y-1",
|
|
79
84
|
children: [/* @__PURE__ */ jsx("h1", {
|
|
80
85
|
className: "text-3xl font-semibold tracking-tight",
|
|
81
|
-
children: "API
|
|
86
|
+
children: "API keys"
|
|
82
87
|
}), /* @__PURE__ */ jsx("p", {
|
|
83
88
|
className: "text-muted-foreground",
|
|
84
89
|
children: "Create API keys for each subscribed plan and use them to authenticate requests to the API gateway."
|
|
@@ -93,7 +98,7 @@ const CredentialsPage = () => {
|
|
|
93
98
|
className: "rounded-xl border border-dashed p-8 text-center",
|
|
94
99
|
children: /* @__PURE__ */ jsx("p", {
|
|
95
100
|
className: "text-muted-foreground",
|
|
96
|
-
children: "API
|
|
101
|
+
children: "API keys are available after the developer portal is published."
|
|
97
102
|
})
|
|
98
103
|
}) : null,
|
|
99
104
|
!isPreview && !isDemoSession && !auth.isAuthenticated ? /* @__PURE__ */ jsxs("div", {
|
|
@@ -110,15 +115,18 @@ const CredentialsPage = () => {
|
|
|
110
115
|
})
|
|
111
116
|
})]
|
|
112
117
|
}) : null,
|
|
113
|
-
|
|
114
|
-
className: "
|
|
115
|
-
|
|
118
|
+
showsLiveData && pageState === "loading" ? /* @__PURE__ */ jsx("div", {
|
|
119
|
+
className: "space-y-6",
|
|
120
|
+
role: "status",
|
|
121
|
+
"aria-busy": "true",
|
|
122
|
+
"aria-label": "Loading API keys",
|
|
123
|
+
children: [0, 1].map((i) => /* @__PURE__ */ jsx("div", { className: "h-40 animate-pulse rounded-xl border bg-muted/40" }, i))
|
|
116
124
|
}) : null,
|
|
117
|
-
|
|
125
|
+
showsLiveData && pageState === "failed" ? /* @__PURE__ */ jsxs("div", {
|
|
118
126
|
className: "rounded-xl border border-dashed p-8 text-center",
|
|
119
127
|
children: [/* @__PURE__ */ jsx("p", {
|
|
120
128
|
className: "text-muted-foreground",
|
|
121
|
-
children: "Unable to load API
|
|
129
|
+
children: "Unable to load your API keys. Try again in a moment."
|
|
122
130
|
}), /* @__PURE__ */ jsx(Button$1, {
|
|
123
131
|
className: "mt-4",
|
|
124
132
|
asChild: true,
|
|
@@ -128,7 +136,8 @@ const CredentialsPage = () => {
|
|
|
128
136
|
})
|
|
129
137
|
})]
|
|
130
138
|
}) : null,
|
|
131
|
-
|
|
139
|
+
showsLiveData && pageState === "empty" ? /* @__PURE__ */ jsx(ChoosePlanNotice, {}) : null,
|
|
140
|
+
showsLiveData && pageState === "ready" ? /* @__PURE__ */ jsxs("div", {
|
|
132
141
|
className: "space-y-6",
|
|
133
142
|
children: [
|
|
134
143
|
planNote ? /* @__PURE__ */ jsx("div", {
|
|
@@ -136,7 +145,7 @@ const CredentialsPage = () => {
|
|
|
136
145
|
className: "rounded-xl border border-primary/30 bg-primary/5 p-4 text-sm",
|
|
137
146
|
children: planNote.text
|
|
138
147
|
}) : null,
|
|
139
|
-
plans.
|
|
148
|
+
plans.map((plan) => /* @__PURE__ */ jsx(ApiKeysPanel, {
|
|
140
149
|
plan,
|
|
141
150
|
header,
|
|
142
151
|
readOnly: isDemoSession
|
|
@@ -169,7 +178,7 @@ const CredentialsPage = () => {
|
|
|
169
178
|
asChild: true,
|
|
170
179
|
children: /* @__PURE__ */ jsx(Link$1, {
|
|
171
180
|
to: "/account/subscriptions",
|
|
172
|
-
children: "
|
|
181
|
+
children: "Billing"
|
|
173
182
|
})
|
|
174
183
|
}), apiReferenceLink ? /* @__PURE__ */ jsx(Button$1, {
|
|
175
184
|
variant: "outline",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-
|
|
1
|
+
import { i as CONTACT_SALES_PATH, r as CHECKOUT_PATH } from "./paths-DaVyTIDJ.mjs";
|
|
2
|
+
import { d as listPlanFeatureLines, i as formatPlanDisplayPrice, l as isPlanCustomPriced, o as formatPlanPriceUnit, s as isActiveSubscription, u as isPlanFree } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
3
3
|
import { Button } from "@lukoweb/apitogo/components";
|
|
4
4
|
import { Link as Link$1 } from "@lukoweb/apitogo/router";
|
|
5
5
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
|
|
@@ -18,7 +18,7 @@ const accountCtaLabel = (plan) => {
|
|
|
18
18
|
};
|
|
19
19
|
/** Custom-priced plans skip checkout and open the contact-sales form. */
|
|
20
20
|
const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
|
|
21
|
-
const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
|
|
21
|
+
const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false, isConfirming = false }) => {
|
|
22
22
|
const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
|
|
23
23
|
const features = listPlanFeatureLines(plan);
|
|
24
24
|
const displayPrice = formatPlanDisplayPrice(plan);
|
|
@@ -83,7 +83,13 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
|
|
|
83
83
|
className: "text-center text-xs text-muted-foreground",
|
|
84
84
|
children: "Unsubscribe is available after the portal is published."
|
|
85
85
|
}) : null
|
|
86
|
-
] }) :
|
|
86
|
+
] }) : isConfirming ? /* @__PURE__ */ jsx(Button, {
|
|
87
|
+
variant: "secondary",
|
|
88
|
+
className: "w-full",
|
|
89
|
+
disabled: true,
|
|
90
|
+
"data-testid": `confirming-${plan.sku}`,
|
|
91
|
+
children: "Confirming payment…"
|
|
92
|
+
}) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
|
|
87
93
|
asChild: true,
|
|
88
94
|
children: /* @__PURE__ */ jsx(Button, {
|
|
89
95
|
variant: featured ? "default" : "outline",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-
|
|
2
|
-
import { t as PricingCard } from "./PricingCard-
|
|
1
|
+
import { f as parseManifestPlans, h as pickFeaturedPlanSku, n as useDevPortalPreviewMode } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
2
|
+
import { t as PricingCard } from "./PricingCard-DFuQEw7Y.mjs";
|
|
3
3
|
import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
-
import { t as usePlans } from "./usePlans-
|
|
5
|
-
import { t as isPreviewPricingEnabled } from "./pricing-config-
|
|
6
|
-
import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-
|
|
4
|
+
import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
|
|
5
|
+
import { t as isPreviewPricingEnabled } from "./pricing-config-Bc6eWFUO.mjs";
|
|
6
|
+
import { a as isPricingLoading, n as buildCompareRows } from "./billing-state-DBQR2tad.mjs";
|
|
7
7
|
import { Head, Heading } from "@lukoweb/apitogo/components";
|
|
8
8
|
import { Navigate } from "@lukoweb/apitogo/router";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -66,15 +66,16 @@ const DEFAULT_COMPARE_ROWS = [
|
|
|
66
66
|
]
|
|
67
67
|
}
|
|
68
68
|
];
|
|
69
|
+
/** Only what the platform itself does, so the defaults never promise anything for the tenant (API-124). */
|
|
69
70
|
const DEFAULT_FAQS = [{
|
|
70
|
-
question: "What happens if I
|
|
71
|
-
answer: "
|
|
71
|
+
question: "What happens if I go over my plan's limits?",
|
|
72
|
+
answer: "Calls over the rate limit or the quota are refused with a 429 response, and its Retry-After header says when to try again."
|
|
72
73
|
}, {
|
|
73
|
-
question: "Can I
|
|
74
|
-
answer: "Yes
|
|
74
|
+
question: "Can I have more than one plan?",
|
|
75
|
+
answer: "Yes. Each plan has its own API keys and limits: create a key for a plan on the API keys page and use it in your app."
|
|
75
76
|
}];
|
|
76
77
|
const PricingPage = () => {
|
|
77
|
-
const { pricing, previewPlans } = useDevPortalBillingConfig();
|
|
78
|
+
const { pricing, previewPlans, featuredPlanSku } = useDevPortalBillingConfig();
|
|
78
79
|
const isPreview = useDevPortalPreviewMode();
|
|
79
80
|
const pricingEnabled = isPreviewPricingEnabled() || isPreview;
|
|
80
81
|
const livePlans = usePlans();
|
|
@@ -92,10 +93,10 @@ const PricingPage = () => {
|
|
|
92
93
|
to: "/",
|
|
93
94
|
replace: true
|
|
94
95
|
});
|
|
95
|
-
const title = pricing?.title ?? "
|
|
96
|
-
const subtitle = pricing?.subtitle ?? "
|
|
96
|
+
const title = pricing?.title ?? "Plans and pricing";
|
|
97
|
+
const subtitle = pricing?.subtitle ?? "Choose the plan that fits how you use the API. Each plan lists its limits and what it includes.";
|
|
97
98
|
const plans = catalog?.plans ?? [];
|
|
98
|
-
const featuredSku = pickFeaturedPlanSku(plans);
|
|
99
|
+
const featuredSku = pickFeaturedPlanSku(plans, featuredPlanSku);
|
|
99
100
|
const compareRows = buildCompareRows({
|
|
100
101
|
plans,
|
|
101
102
|
configuredRows: pricing?.compareRows,
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { a as CREDENTIALS_PATH, n as ALREADY_SUBSCRIBED_PARAM, t as ACCOUNT_PLANS_PATH } from "./paths-DaVyTIDJ.mjs";
|
|
2
|
+
import { a as formatPlanPrice, p as parseMonthlyQuotaCalls, s as isActiveSubscription, t as useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl-BiI2jFjU.mjs";
|
|
3
|
+
import { n as useDevPortalBillingConfig } from "./DevPortalBillingContext-CR83s4LO.mjs";
|
|
4
|
+
import { t as useMySubscriptions } from "./useMySubscriptions-BYVIIWbe.mjs";
|
|
5
|
+
import { t as usePlans } from "./usePlans-HyhIIuJZ.mjs";
|
|
6
|
+
import { n as readPendingCheckout, t as clearPendingCheckout } from "./pending-checkout-DpVaNnTu.mjs";
|
|
7
|
+
import { n as useApiKeys } from "./useApiKeys-BOY3vIOr.mjs";
|
|
8
|
+
import { t as useUsage } from "./useUsage-B5x9YuKa.mjs";
|
|
9
|
+
import { i as hasPaymentProvider, r as checkoutConfirmingText } from "./billing-state-DBQR2tad.mjs";
|
|
10
|
+
import { i as ChoosePlanNotice, n as billingPageState, r as keyPlanNote } from "./page-state-BLL2JJBc.mjs";
|
|
11
|
+
import { Head, Spinner } from "@lukoweb/apitogo/components";
|
|
12
|
+
import { Link as Link$1, useSearchParams } from "@lukoweb/apitogo/router";
|
|
13
|
+
import { useEffect } from "react";
|
|
14
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
15
|
+
import { useMutation } from "@lukoweb/apitogo/react-query";
|
|
16
|
+
import { useAuth } from "@lukoweb/apitogo/hooks";
|
|
17
|
+
import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button.js";
|
|
18
|
+
|
|
19
|
+
//#region src/hooks/useBillingPortal.ts
|
|
20
|
+
function useBillingPortal() {
|
|
21
|
+
const apiBaseUrl = useDevPortalApiBaseUrl();
|
|
22
|
+
return useMutation({ mutationFn: async (returnUrl) => {
|
|
23
|
+
if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
|
|
24
|
+
const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/billing-portal`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"Content-Type": "application/json"
|
|
29
|
+
},
|
|
30
|
+
credentials: "include",
|
|
31
|
+
body: JSON.stringify({ returnUrl: returnUrl ?? `${window.location.origin}/account/subscriptions` })
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) throw new Error("Unable to open billing portal.");
|
|
34
|
+
const body = await response.json();
|
|
35
|
+
if (!body.url) throw new Error("Billing portal URL was not returned.");
|
|
36
|
+
window.location.assign(body.url);
|
|
37
|
+
} });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/pages/SubscriptionsPage.tsx
|
|
42
|
+
/** How an earlier plan ended, in words a customer reads. */
|
|
43
|
+
function subscriptionStatusLabel(status) {
|
|
44
|
+
switch (status.toLowerCase().replace(/_/g, "")) {
|
|
45
|
+
case "canceled": return "Ended";
|
|
46
|
+
case "pastdue": return "Payment due";
|
|
47
|
+
case "incomplete": return "Not started";
|
|
48
|
+
default: return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const SubscriptionsPage = () => {
|
|
52
|
+
const { billing } = useDevPortalBillingConfig();
|
|
53
|
+
const auth = useAuth();
|
|
54
|
+
const [searchParams] = useSearchParams();
|
|
55
|
+
const checkoutSuccess = searchParams.get("checkout") === "success";
|
|
56
|
+
const alreadySubscribedSku = searchParams.get(ALREADY_SUBSCRIBED_PARAM);
|
|
57
|
+
const checkoutPaid = searchParams.get("paid") !== "0";
|
|
58
|
+
const { data: me, error: meError, refetch } = useMySubscriptions();
|
|
59
|
+
const billingState = billingPageState({
|
|
60
|
+
data: me,
|
|
61
|
+
error: meError
|
|
62
|
+
});
|
|
63
|
+
const isLoading = billingState === "loading";
|
|
64
|
+
const { data: catalog } = usePlans();
|
|
65
|
+
const billingPortal = useBillingPortal();
|
|
66
|
+
const { data: usage } = useUsage("30d");
|
|
67
|
+
const { data: apiKeys } = useApiKeys();
|
|
68
|
+
const planNote = keyPlanNote(apiKeys?.plans ?? []);
|
|
69
|
+
const planBySku = new Map((catalog?.plans ?? []).map((plan) => [plan.sku, plan]));
|
|
70
|
+
const subscriptions = me?.subscriptions ?? [];
|
|
71
|
+
const activeSubscriptions = subscriptions.filter((sub) => isActiveSubscription(sub.status));
|
|
72
|
+
const earlierSubscriptions = subscriptions.filter((sub) => !isActiveSubscription(sub.status));
|
|
73
|
+
const hasActivePlan = activeSubscriptions.length > 0;
|
|
74
|
+
const singlePlan = activeSubscriptions.length === 1 ? planBySku.get(activeSubscriptions[0].planSku) : void 0;
|
|
75
|
+
const pendingSku = checkoutSuccess ? readPendingCheckout() : void 0;
|
|
76
|
+
const pendingActive = pendingSku !== void 0 && activeSubscriptions.some((sub) => sub.planSku === pendingSku);
|
|
77
|
+
const confirmingPayment = checkoutSuccess && !isLoading && (pendingSku !== void 0 ? !pendingActive : !hasActivePlan);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (pendingActive) clearPendingCheckout();
|
|
80
|
+
}, [pendingActive]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!confirmingPayment) return;
|
|
83
|
+
let attempts = 0;
|
|
84
|
+
const timer = setInterval(() => {
|
|
85
|
+
attempts += 1;
|
|
86
|
+
refetch();
|
|
87
|
+
if (attempts >= 30) clearInterval(timer);
|
|
88
|
+
}, 2e3);
|
|
89
|
+
return () => clearInterval(timer);
|
|
90
|
+
}, [confirmingPayment, refetch]);
|
|
91
|
+
const monthlyQuota = singlePlan ? parseMonthlyQuotaCalls(singlePlan.limitsJson) : null;
|
|
92
|
+
const usedRequests = usage?.requestCount ?? 0;
|
|
93
|
+
const quotaPct = monthlyQuota && monthlyQuota > 0 ? Math.min(100, Math.round(usedRequests / monthlyQuota * 100)) : null;
|
|
94
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
95
|
+
className: "px-8 py-7",
|
|
96
|
+
children: [
|
|
97
|
+
/* @__PURE__ */ jsx(Head, { children: /* @__PURE__ */ jsx("title", { children: "Billing" }) }),
|
|
98
|
+
/* @__PURE__ */ jsx("h1", {
|
|
99
|
+
className: "mb-2 text-[1.6rem] font-bold tracking-[-0.01em]",
|
|
100
|
+
children: "Billing"
|
|
101
|
+
}),
|
|
102
|
+
/* @__PURE__ */ jsx("p", {
|
|
103
|
+
className: "mb-8 text-muted-foreground",
|
|
104
|
+
children: "Manage your plans, payment method, and invoices."
|
|
105
|
+
}),
|
|
106
|
+
alreadySubscribedSku ? /* @__PURE__ */ jsxs("div", {
|
|
107
|
+
role: "status",
|
|
108
|
+
className: "mb-6 rounded-xl border border-primary/30 bg-primary/5 p-4 text-sm",
|
|
109
|
+
children: [
|
|
110
|
+
"You're already on the",
|
|
111
|
+
" ",
|
|
112
|
+
planBySku.get(alreadySubscribedSku)?.displayName ?? alreadySubscribedSku,
|
|
113
|
+
" ",
|
|
114
|
+
"plan. Go to",
|
|
115
|
+
" ",
|
|
116
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
117
|
+
to: CREDENTIALS_PATH,
|
|
118
|
+
className: "font-medium underline",
|
|
119
|
+
children: "API keys"
|
|
120
|
+
}),
|
|
121
|
+
" ",
|
|
122
|
+
"to create a key."
|
|
123
|
+
]
|
|
124
|
+
}) : null,
|
|
125
|
+
confirmingPayment ? /* @__PURE__ */ jsx("div", {
|
|
126
|
+
className: "mb-6 rounded-xl border border-border bg-card p-4 text-sm",
|
|
127
|
+
children: checkoutConfirmingText(checkoutPaid)
|
|
128
|
+
}) : null,
|
|
129
|
+
checkoutSuccess && hasActivePlan && !confirmingPayment ? /* @__PURE__ */ jsx("div", {
|
|
130
|
+
className: "mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm",
|
|
131
|
+
children: planNote ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
132
|
+
planNote.text,
|
|
133
|
+
" ",
|
|
134
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
135
|
+
to: CREDENTIALS_PATH,
|
|
136
|
+
className: "font-medium underline",
|
|
137
|
+
children: "Create the key"
|
|
138
|
+
}),
|
|
139
|
+
"."
|
|
140
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
141
|
+
"Your subscription is active.",
|
|
142
|
+
" ",
|
|
143
|
+
/* @__PURE__ */ jsx(Link$1, {
|
|
144
|
+
to: CREDENTIALS_PATH,
|
|
145
|
+
className: "font-medium underline",
|
|
146
|
+
children: "View your API keys"
|
|
147
|
+
}),
|
|
148
|
+
"."
|
|
149
|
+
] })
|
|
150
|
+
}) : null,
|
|
151
|
+
isLoading ? /* @__PURE__ */ jsx("div", {
|
|
152
|
+
className: "mb-8 h-40 animate-pulse rounded-xl border bg-muted/40",
|
|
153
|
+
role: "status",
|
|
154
|
+
"aria-busy": "true",
|
|
155
|
+
"aria-label": "Loading billing details"
|
|
156
|
+
}) : null,
|
|
157
|
+
billingState === "failed" ? /* @__PURE__ */ jsx("p", {
|
|
158
|
+
className: "mb-8 text-muted-foreground",
|
|
159
|
+
children: "Unable to load your plans. Try again in a moment."
|
|
160
|
+
}) : null,
|
|
161
|
+
!isLoading && hasActivePlan ? /* @__PURE__ */ jsxs("section", {
|
|
162
|
+
className: "mb-8",
|
|
163
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
164
|
+
className: "mb-3 text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
165
|
+
children: activeSubscriptions.length > 1 ? "Your plans" : "Current plan"
|
|
166
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
167
|
+
className: "space-y-4",
|
|
168
|
+
children: activeSubscriptions.map((subscription) => {
|
|
169
|
+
const plan = planBySku.get(subscription.planSku);
|
|
170
|
+
return /* @__PURE__ */ jsxs("article", {
|
|
171
|
+
className: "rounded-xl border border-border bg-card p-6",
|
|
172
|
+
"data-testid": `active-plan-${subscription.planSku}`,
|
|
173
|
+
children: [
|
|
174
|
+
/* @__PURE__ */ jsx("h2", {
|
|
175
|
+
className: "text-2xl font-bold",
|
|
176
|
+
children: plan?.displayName ?? subscription.planSku
|
|
177
|
+
}),
|
|
178
|
+
plan ? /* @__PURE__ */ jsx("p", {
|
|
179
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
180
|
+
children: formatPlanPrice(plan)
|
|
181
|
+
}) : /* @__PURE__ */ jsx("p", {
|
|
182
|
+
className: "mt-1 text-sm text-muted-foreground",
|
|
183
|
+
children: "This plan is no longer offered. Your access continues until you cancel it."
|
|
184
|
+
}),
|
|
185
|
+
subscription.currentPeriodEnd ? /* @__PURE__ */ jsxs("p", {
|
|
186
|
+
className: "mt-3 text-sm text-muted-foreground",
|
|
187
|
+
children: [
|
|
188
|
+
"Renews",
|
|
189
|
+
" ",
|
|
190
|
+
new Date(subscription.currentPeriodEnd).toLocaleDateString()
|
|
191
|
+
]
|
|
192
|
+
}) : null,
|
|
193
|
+
singlePlan && quotaPct != null && monthlyQuota != null ? /* @__PURE__ */ jsxs("div", {
|
|
194
|
+
className: "mt-5",
|
|
195
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
196
|
+
className: "mb-1.5 flex justify-between text-xs text-muted-foreground",
|
|
197
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Monthly requests" }), /* @__PURE__ */ jsxs("span", { children: [
|
|
198
|
+
usedRequests.toLocaleString(),
|
|
199
|
+
" /",
|
|
200
|
+
" ",
|
|
201
|
+
monthlyQuota.toLocaleString()
|
|
202
|
+
] })]
|
|
203
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
204
|
+
className: "h-2 rounded-full bg-muted",
|
|
205
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
206
|
+
className: "h-full rounded-full bg-primary transition-[width]",
|
|
207
|
+
style: { width: `${quotaPct}%` }
|
|
208
|
+
})
|
|
209
|
+
})]
|
|
210
|
+
}) : null
|
|
211
|
+
]
|
|
212
|
+
}, subscription.planSku);
|
|
213
|
+
})
|
|
214
|
+
})]
|
|
215
|
+
}) : null,
|
|
216
|
+
billingState === "empty" && !confirmingPayment ? /* @__PURE__ */ jsx(ChoosePlanNotice, {
|
|
217
|
+
className: "mb-8 rounded-xl border border-dashed p-8 text-center",
|
|
218
|
+
body: "You do not have a plan yet. Choose one to start using the API; your billing details appear here afterwards."
|
|
219
|
+
}) : null,
|
|
220
|
+
!isLoading && earlierSubscriptions.length > 0 ? /* @__PURE__ */ jsxs("section", {
|
|
221
|
+
className: "mb-8",
|
|
222
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
223
|
+
className: "mb-2 text-xs font-medium uppercase tracking-[0.05em] text-muted-foreground",
|
|
224
|
+
children: "Earlier plans"
|
|
225
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
226
|
+
className: "divide-y rounded-xl border bg-card text-sm",
|
|
227
|
+
children: earlierSubscriptions.map((subscription) => /* @__PURE__ */ jsxs("li", {
|
|
228
|
+
className: "flex items-center justify-between gap-4 px-5 py-3",
|
|
229
|
+
children: [/* @__PURE__ */ jsx("span", { children: planBySku.get(subscription.planSku)?.displayName ?? subscription.planSku }), /* @__PURE__ */ jsx("span", {
|
|
230
|
+
className: "text-xs text-muted-foreground",
|
|
231
|
+
children: subscriptionStatusLabel(subscription.status)
|
|
232
|
+
})]
|
|
233
|
+
}, `${subscription.planSku}-${subscription.status}`))
|
|
234
|
+
})]
|
|
235
|
+
}) : null,
|
|
236
|
+
/* @__PURE__ */ jsxs("div", {
|
|
237
|
+
className: "flex flex-wrap gap-3",
|
|
238
|
+
children: [
|
|
239
|
+
/* @__PURE__ */ jsx(Button$1, {
|
|
240
|
+
variant: "outline",
|
|
241
|
+
asChild: true,
|
|
242
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
243
|
+
to: ACCOUNT_PLANS_PATH,
|
|
244
|
+
children: "Manage plans"
|
|
245
|
+
})
|
|
246
|
+
}),
|
|
247
|
+
auth.isAuthenticated && hasPaymentProvider(billing) ? /* @__PURE__ */ jsx(Button$1, {
|
|
248
|
+
variant: "default",
|
|
249
|
+
disabled: billingPortal.isPending,
|
|
250
|
+
onClick: () => {
|
|
251
|
+
billingPortal.mutate();
|
|
252
|
+
},
|
|
253
|
+
children: billingPortal.isPending ? /* @__PURE__ */ jsx(Spinner, {}) : "Manage payment & invoices"
|
|
254
|
+
}) : null,
|
|
255
|
+
auth.isAuthenticated ? /* @__PURE__ */ jsx(Button$1, {
|
|
256
|
+
variant: "secondary",
|
|
257
|
+
asChild: true,
|
|
258
|
+
children: /* @__PURE__ */ jsx(Link$1, {
|
|
259
|
+
to: CREDENTIALS_PATH,
|
|
260
|
+
children: "API keys"
|
|
261
|
+
})
|
|
262
|
+
}) : null
|
|
263
|
+
]
|
|
264
|
+
}),
|
|
265
|
+
billingPortal.isError ? /* @__PURE__ */ jsx("p", {
|
|
266
|
+
className: "mt-4 text-sm text-destructive",
|
|
267
|
+
children: billingPortal.error instanceof Error ? billingPortal.error.message : "Unable to open billing portal."
|
|
268
|
+
}) : null
|
|
269
|
+
]
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
export { SubscriptionsPage as default, subscriptionStatusLabel };
|