@lukoweb/apitogo-plugin-dev-portal-billing 0.1.41 → 0.1.43

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.
@@ -1,106 +0,0 @@
1
- import { Head } from "@lukoweb/apitogo/components";
2
- import { CreditCardIcon } from "@lukoweb/apitogo/icons";
3
- import { useState } from "react";
4
- import { PricingCard } from "../components/PricingCard.js";
5
- import { useCancelSubscription } from "../hooks/useCancelSubscription.js";
6
- import { useDevPortalPreviewMode } from "../hooks/useDevPortalApiBaseUrl.js";
7
- import { useMySubscriptions } from "../hooks/useMySubscriptions.js";
8
- import { usePlans } from "../hooks/usePlans.js";
9
- import { isActiveSubscription } from "../utils/dev-portal-api.js";
10
-
11
- const AccountPlansPage = () => {
12
- const isPreview = useDevPortalPreviewMode();
13
- const { data: catalog, isLoading, isError } = usePlans();
14
- const { data: me } = useMySubscriptions();
15
- const cancelMutation = useCancelSubscription();
16
- const [cancelingSku, setCancelingSku] = useState<string | null>(null);
17
-
18
- const subscriptionBySku = new Map(
19
- (me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]),
20
- );
21
-
22
- const handleUnsubscribe = async (planSku: string) => {
23
- setCancelingSku(planSku);
24
- try {
25
- await cancelMutation.mutateAsync(planSku);
26
- } finally {
27
- setCancelingSku(null);
28
- }
29
- };
30
-
31
- return (
32
- <div className="mx-auto max-w-screen-xl px-4 pb-16 pt-(--padding-content-top) lg:px-8">
33
- <Head>
34
- <title>Plans & billing</title>
35
- </Head>
36
-
37
- <div className="mb-8 flex items-start gap-4">
38
- <div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
39
- <CreditCardIcon size={24} />
40
- </div>
41
- <div className="space-y-1">
42
- <h1 className="text-3xl font-semibold tracking-tight">
43
- Plans & billing
44
- </h1>
45
- <p className="max-w-2xl text-muted-foreground">
46
- Compare plans, manage your subscriptions, and change tiers.
47
- </p>
48
- </div>
49
- </div>
50
-
51
- {isPreview ? (
52
- <div className="mb-8 rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
53
- Local preview uses mock plans. Subscribe and unsubscribe activate
54
- after the portal is published.
55
- </div>
56
- ) : null}
57
-
58
- {cancelMutation.isError ? (
59
- <div className="mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive">
60
- {cancelMutation.error instanceof Error
61
- ? cancelMutation.error.message
62
- : "Failed to cancel subscription."}
63
- </div>
64
- ) : null}
65
-
66
- {isLoading ? (
67
- <p className="text-muted-foreground">Loading plans…</p>
68
- ) : null}
69
-
70
- {isError ? (
71
- <p className="text-destructive">Unable to load plans.</p>
72
- ) : null}
73
-
74
- {catalog && catalog.plans.length > 0 ? (
75
- <div className="grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]">
76
- {catalog.plans.map((plan) => {
77
- const subscription = subscriptionBySku.get(plan.sku);
78
- const isSubscribed = isActiveSubscription(
79
- subscription?.status ?? "",
80
- );
81
-
82
- return (
83
- <PricingCard
84
- key={plan.sku}
85
- plan={plan}
86
- mode="account"
87
- subscriptionStatus={subscription?.status}
88
- isSubscribed={isSubscribed}
89
- onUnsubscribe={isPreview ? undefined : handleUnsubscribe}
90
- isCanceling={cancelingSku === plan.sku}
91
- cancelDisabled={isPreview}
92
- />
93
- );
94
- })}
95
- </div>
96
- ) : !isLoading && !isError ? (
97
- <div className="rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground">
98
- No plans are available yet. Sync plans from your APItoGo manifest or
99
- platform gateway.
100
- </div>
101
- ) : null}
102
- </div>
103
- );
104
- };
105
-
106
- export default AccountPlansPage;
@@ -1,127 +0,0 @@
1
- import { useAuth } from "@lukoweb/apitogo/hooks";
2
- import { ShieldIcon } from "@lukoweb/apitogo/icons";
3
- import { useQuery } from "@lukoweb/apitogo/react-query";
4
- import { Link, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
5
- import {
6
- Alert,
7
- AlertAction,
8
- AlertDescription,
9
- } from "@lukoweb/apitogo/ui/Alert";
10
- import { Button } from "@lukoweb/apitogo/ui/Button";
11
- import { useEffect } from "react";
12
- import { RedirectPage } from "../components/RedirectPage.js";
13
- import {
14
- useDevPortalApiBaseUrl,
15
- useDevPortalPreviewMode,
16
- } from "../hooks/useDevPortalApiBaseUrl.js";
17
- import { PRICING_PATH } from "../paths.js";
18
- import type { CheckoutResponse } from "../types.js";
19
-
20
- const ACCOUNT_PLANS_PATH = "/account/plans";
21
-
22
- const CheckoutPage = () => {
23
- const [searchParams] = useSearchParams();
24
- const planSku = searchParams.get("planSku") ?? "";
25
- const navigate = useNavigate();
26
- const auth = useAuth();
27
- const apiBaseUrl = useDevPortalApiBaseUrl();
28
- const isPreview = useDevPortalPreviewMode();
29
- const plansPath = auth.isAuthenticated ? ACCOUNT_PLANS_PATH : PRICING_PATH;
30
-
31
- const checkoutQuery = useQuery<CheckoutResponse>({
32
- queryKey: ["dev-portal-checkout", apiBaseUrl, planSku, auth.profile?.sub],
33
- enabled:
34
- Boolean(planSku) &&
35
- !isPreview &&
36
- auth.isAuthenticated &&
37
- Boolean(apiBaseUrl),
38
- retry: false,
39
- queryFn: async () => {
40
- const response = await fetch(
41
- `${apiBaseUrl}/api/v1/subscriptions/checkout`,
42
- {
43
- method: "POST",
44
- headers: {
45
- Accept: "application/json",
46
- "Content-Type": "application/json",
47
- },
48
- credentials: "include",
49
- body: JSON.stringify({ planSku }),
50
- },
51
- );
52
-
53
- if (!response.ok) {
54
- const detail = await response.text();
55
- throw new Error(detail || "Checkout failed.");
56
- }
57
-
58
- return (await response.json()) as CheckoutResponse;
59
- },
60
- });
61
-
62
- const freeProvisioned =
63
- checkoutQuery.data?.isFree === true &&
64
- checkoutQuery.data.entitlementProvisioned === true;
65
-
66
- useEffect(() => {
67
- if (freeProvisioned) {
68
- navigate("/account/subscriptions?checkout=success", { replace: true });
69
- }
70
- }, [freeProvisioned, navigate]);
71
-
72
- if (!planSku) {
73
- return (
74
- <div className="mx-auto max-w-lg px-4 py-16 text-center text-muted-foreground">
75
- Missing plan SKU.{" "}
76
- <Link to={plansPath} className="text-primary underline">
77
- Back to plans
78
- </Link>
79
- </div>
80
- );
81
- }
82
-
83
- if (isPreview) {
84
- return (
85
- <div className="mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center">
86
- <h1 className="text-2xl font-semibold tracking-tight">
87
- Checkout unlocks after publish
88
- </h1>
89
- <p className="text-muted-foreground">
90
- Subscribe and billing flows require a live dev-portal backend. Publish
91
- your portal to APItoGo, then return here to complete checkout.
92
- </p>
93
- <Button asChild variant="outline">
94
- <Link to={plansPath}>Back to plans</Link>
95
- </Button>
96
- </div>
97
- );
98
- }
99
-
100
- if (freeProvisioned) {
101
- return null;
102
- }
103
-
104
- return (
105
- <RedirectPage
106
- icon={ShieldIcon}
107
- title="Establishing secure checkout…"
108
- description="Redirecting you to complete your subscription."
109
- url={checkoutQuery.data?.checkoutUrl ?? undefined}
110
- >
111
- {checkoutQuery.isError ? (
112
- <Alert variant="destructive">
113
- <AlertDescription className="first-letter:uppercase">
114
- {checkoutQuery.error.message}
115
- </AlertDescription>
116
- <AlertAction>
117
- <Button variant="outline" size="xs" asChild>
118
- <Link to={plansPath}>Back to plans</Link>
119
- </Button>
120
- </AlertAction>
121
- </Alert>
122
- ) : null}
123
- </RedirectPage>
124
- );
125
- };
126
-
127
- export default CheckoutPage;
@@ -1,145 +0,0 @@
1
- import { Head } from "@lukoweb/apitogo/components";
2
- import { useAuth } from "@lukoweb/apitogo/hooks";
3
- import { KeyRoundIcon } from "@lukoweb/apitogo/icons";
4
- import { Link } from "@lukoweb/apitogo/router";
5
- import { Button } from "@lukoweb/apitogo/ui/Button";
6
- import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
7
- import { useState } from "react";
8
- import { useCredentials } from "../hooks/useCredentials.js";
9
- import { useDevPortalPreviewMode } from "../hooks/useDevPortalApiBaseUrl.js";
10
- import { usePlans } from "../hooks/usePlans.js";
11
-
12
- const CredentialsPage = () => {
13
- const auth = useAuth();
14
- const isPreview = useDevPortalPreviewMode();
15
- const { data: credentials, isLoading, error } = useCredentials();
16
- const { data: catalog } = usePlans();
17
- const [revealed, setRevealed] = useState(false);
18
-
19
- const plan = catalog?.plans.find((item) => item.sku === credentials?.planSku);
20
-
21
- return (
22
- <div className="mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8">
23
- <Head>
24
- <title>API credentials</title>
25
- </Head>
26
-
27
- <div className="mb-8 flex items-start gap-4">
28
- <div className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
29
- <KeyRoundIcon size={24} />
30
- </div>
31
- <div className="space-y-1">
32
- <h1 className="text-3xl font-semibold tracking-tight">
33
- API credentials
34
- </h1>
35
- <p className="text-muted-foreground">
36
- Use your subscription key to authenticate requests to the API
37
- gateway.
38
- </p>
39
- </div>
40
- </div>
41
-
42
- {isPreview ? (
43
- <div className="rounded-xl border border-dashed p-8 text-center">
44
- <p className="text-muted-foreground">
45
- API credentials are available after the developer portal is
46
- published.
47
- </p>
48
- </div>
49
- ) : null}
50
-
51
- {!isPreview && !auth.isAuthenticated ? (
52
- <div className="rounded-xl border border-dashed p-8 text-center">
53
- <p className="text-muted-foreground">
54
- Sign in to view your subscription key.
55
- </p>
56
- <Button className="mt-4" asChild>
57
- <Link to="/account/plans">View plans</Link>
58
- </Button>
59
- </div>
60
- ) : null}
61
-
62
- {!isPreview && auth.isAuthenticated && isLoading ? (
63
- <p className="text-muted-foreground">Loading credentials…</p>
64
- ) : null}
65
-
66
- {!isPreview && auth.isAuthenticated && error ? (
67
- <div className="rounded-xl border border-dashed p-8 text-center">
68
- <p className="text-muted-foreground">
69
- {error instanceof Error && error.message === "NO_ENTITLEMENT"
70
- ? "No active subscription found. Choose a plan to get your API key."
71
- : "Unable to load API credentials. Try again in a moment."}
72
- </p>
73
- <Button className="mt-4" asChild>
74
- <Link to="/account/plans">View plans</Link>
75
- </Button>
76
- </div>
77
- ) : null}
78
-
79
- {!isPreview && credentials ? (
80
- <div className="space-y-6">
81
- <article className="rounded-xl border bg-card/60 p-6">
82
- <div className="space-y-1">
83
- <h2 className="text-lg font-medium">Active plan</h2>
84
- <p className="text-sm text-muted-foreground">
85
- {plan?.displayName ?? credentials.planSku}
86
- </p>
87
- </div>
88
- </article>
89
-
90
- <article className="rounded-xl border bg-card/60 p-6 space-y-4">
91
- <div className="space-y-1">
92
- <h2 className="text-lg font-medium">Subscription key</h2>
93
- <p className="text-sm text-muted-foreground">
94
- Send this value in the{" "}
95
- <code className="rounded bg-muted px-1 py-0.5 text-xs">
96
- Ocp-Apim-Subscription-Key
97
- </code>{" "}
98
- header when calling the gateway.
99
- </p>
100
- </div>
101
- <Secret
102
- className="max-w-full w-full"
103
- secret={credentials.subscriptionKey}
104
- status="active"
105
- revealed={revealed}
106
- onReveal={setRevealed}
107
- />
108
- </article>
109
-
110
- <article className="rounded-xl border bg-card/60 p-6 space-y-4">
111
- <div className="space-y-1">
112
- <h2 className="text-lg font-medium">Gateway URL</h2>
113
- <p className="text-sm text-muted-foreground">
114
- Base URL for API requests through the gateway.
115
- </p>
116
- </div>
117
- {credentials.gatewayBaseUrl ? (
118
- <Secret
119
- className="max-w-full w-full"
120
- secret={credentials.gatewayBaseUrl}
121
- status="none"
122
- noReveal
123
- />
124
- ) : (
125
- <p className="text-sm text-muted-foreground">
126
- Available after your API is deployed to the gateway.
127
- </p>
128
- )}
129
- </article>
130
- </div>
131
- ) : null}
132
-
133
- <div className="mt-8 flex flex-wrap gap-3">
134
- <Button variant="outline" asChild>
135
- <Link to="/account/subscriptions">My subscriptions</Link>
136
- </Button>
137
- <Button variant="outline" asChild>
138
- <Link to="/api">API reference</Link>
139
- </Button>
140
- </div>
141
- </div>
142
- );
143
- };
144
-
145
- export default CredentialsPage;
@@ -1,87 +0,0 @@
1
- import { Head, Heading } from "@lukoweb/apitogo/components";
2
- import { useAuth } from "@lukoweb/apitogo/hooks";
3
- import { Navigate } from "@lukoweb/apitogo/router";
4
- import { PricingCard } from "../components/PricingCard.js";
5
- import { useDevPortalBillingConfig } from "../DevPortalBillingContext.js";
6
- import { useDevPortalPreviewMode } from "../hooks/useDevPortalApiBaseUrl.js";
7
- import { useMySubscriptions } from "../hooks/useMySubscriptions.js";
8
- import { usePlans } from "../hooks/usePlans.js";
9
- import { isActiveSubscription } from "../utils/dev-portal-api.js";
10
-
11
- const PricingPage = () => {
12
- const { pricing } = useDevPortalBillingConfig();
13
- const auth = useAuth();
14
- const isPreview = useDevPortalPreviewMode();
15
- const { data: catalog, isLoading, isError } = usePlans();
16
- const { data: me } = useMySubscriptions();
17
-
18
- if (auth.isAuthenticated) {
19
- return <Navigate to="/account/plans" replace />;
20
- }
21
-
22
- const activeSkus = new Set(
23
- (me?.subscriptions ?? [])
24
- .filter((sub) => isActiveSubscription(sub.status))
25
- .map((sub) => sub.planSku),
26
- );
27
-
28
- const title = pricing?.title ?? "Pricing";
29
- const subtitle =
30
- pricing?.subtitle ??
31
- "Compare plans and subscribe when you are ready to integrate.";
32
-
33
- return (
34
- <div className="w-full px-4 pt-(--padding-content-top) pb-(--padding-content-bottom)">
35
- <Head>
36
- <title>{title}</title>
37
- <meta name="description" content={subtitle} />
38
- </Head>
39
- <div className="mb-12 space-y-4 text-center">
40
- <Heading level={1} data-testid="title">
41
- {title}
42
- </Heading>
43
- <p className="text-muted-foreground" data-testid="subtitle">
44
- {subtitle}
45
- </p>
46
- </div>
47
-
48
- {isPreview && !auth.isAuthenticated ? (
49
- <div className="mx-auto mb-8 max-w-2xl rounded-xl border border-dashed bg-muted/20 p-6 text-center text-sm text-muted-foreground">
50
- Local preview uses mock plans from{" "}
51
- <code className="rounded bg-muted px-1.5 py-0.5">
52
- VITE_APITOGO_DEV_PORTAL_MOCK_PLANS
53
- </code>
54
- . Checkout activates after the portal is published.
55
- </div>
56
- ) : null}
57
-
58
- {isLoading ? (
59
- <p className="text-center text-muted-foreground">Loading plans…</p>
60
- ) : null}
61
-
62
- {isError ? (
63
- <p className="text-center text-destructive">Unable to load plans.</p>
64
- ) : null}
65
-
66
- {catalog && catalog.plans.length > 0 ? (
67
- <div className="grid grid-cols-1 justify-center gap-6 sm:grid-cols-[repeat(auto-fit,minmax(280px,max-content))]">
68
- {catalog.plans.map((plan) => (
69
- <PricingCard
70
- key={plan.sku}
71
- plan={plan}
72
- mode="public"
73
- isSubscribed={activeSkus.has(plan.sku)}
74
- />
75
- ))}
76
- </div>
77
- ) : !isLoading && !isError ? (
78
- <div className="mx-auto max-w-lg rounded-xl border border-dashed p-8 text-center text-sm text-muted-foreground">
79
- No plans are available yet. Sync plans from your APItoGo manifest or
80
- platform gateway.
81
- </div>
82
- ) : null}
83
- </div>
84
- );
85
- };
86
-
87
- export default PricingPage;
@@ -1,129 +0,0 @@
1
- import { Head } from "@lukoweb/apitogo/components";
2
- import { useAuth } from "@lukoweb/apitogo/hooks";
3
- import { Link, useSearchParams } from "@lukoweb/apitogo/router";
4
- import { Button } from "@lukoweb/apitogo/ui/Button";
5
- import { useMySubscriptions } from "../hooks/useMySubscriptions.js";
6
- import { usePlans } from "../hooks/usePlans.js";
7
- import { CREDENTIALS_PATH } from "../paths.js";
8
- import {
9
- formatBillingPeriod,
10
- formatPlanPrice,
11
- isActiveSubscription,
12
- } from "../utils/dev-portal-api.js";
13
-
14
- const SubscriptionsPage = () => {
15
- const auth = useAuth();
16
- const [searchParams] = useSearchParams();
17
- const checkoutSuccess = searchParams.get("checkout") === "success";
18
- const { data: me, isLoading } = useMySubscriptions();
19
- const { data: catalog } = usePlans();
20
-
21
- const planBySku = new Map(
22
- (catalog?.plans ?? []).map((plan) => [plan.sku, plan]),
23
- );
24
-
25
- const subscriptions = me?.subscriptions ?? [];
26
-
27
- return (
28
- <div className="mx-auto max-w-screen-lg px-4 pb-16 pt-(--padding-content-top) lg:px-8">
29
- <Head>
30
- <title>My subscriptions</title>
31
- </Head>
32
-
33
- <div className="mb-8 space-y-2">
34
- <h1 className="text-3xl font-semibold tracking-tight">
35
- My subscriptions
36
- </h1>
37
- <p className="text-muted-foreground">
38
- Review your active plans and manage billing.
39
- </p>
40
- </div>
41
-
42
- {checkoutSuccess ? (
43
- <div className="mb-6 rounded-xl border border-green-500/30 bg-green-500/10 p-4 text-sm">
44
- Your subscription is active.{" "}
45
- <Link to={CREDENTIALS_PATH} className="font-medium underline">
46
- View your API credentials
47
- </Link>
48
- .
49
- </div>
50
- ) : null}
51
-
52
- {isLoading ? (
53
- <p className="text-muted-foreground">Loading subscriptions…</p>
54
- ) : null}
55
-
56
- {!isLoading && subscriptions.length === 0 ? (
57
- <div className="rounded-xl border border-dashed p-8 text-center">
58
- <p className="text-muted-foreground">
59
- You do not have an active subscription yet.
60
- </p>
61
- <Button className="mt-4" asChild>
62
- <Link to="/account/plans">View plans</Link>
63
- </Button>
64
- </div>
65
- ) : null}
66
-
67
- {subscriptions.length > 0 ? (
68
- <div className="space-y-4">
69
- {subscriptions.map((subscription) => {
70
- const plan = planBySku.get(subscription.planSku);
71
- return (
72
- <article
73
- key={`${subscription.planSku}-${subscription.status}`}
74
- className="rounded-xl border bg-card/60 p-6"
75
- >
76
- <div className="flex flex-wrap items-start justify-between gap-4">
77
- <div>
78
- <h2 className="text-lg font-medium">
79
- {plan?.displayName ?? subscription.planSku}
80
- </h2>
81
- {plan ? (
82
- <p className="mt-1 text-sm text-muted-foreground">
83
- {formatPlanPrice(plan)} ·{" "}
84
- {formatBillingPeriod(plan.billingPeriod)}
85
- </p>
86
- ) : null}
87
- </div>
88
- <span
89
- className={
90
- isActiveSubscription(subscription.status)
91
- ? "rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary"
92
- : "rounded-full bg-muted px-3 py-1 text-xs font-medium text-muted-foreground"
93
- }
94
- >
95
- {subscription.status}
96
- </span>
97
- </div>
98
- {subscription.currentPeriodEnd ? (
99
- <p className="mt-3 text-sm text-muted-foreground">
100
- Current period ends{" "}
101
- {new Date(
102
- subscription.currentPeriodEnd,
103
- ).toLocaleDateString()}
104
- </p>
105
- ) : null}
106
- </article>
107
- );
108
- })}
109
- </div>
110
- ) : null}
111
-
112
- <div className="mt-8 flex flex-wrap gap-3">
113
- <Button variant="outline" asChild>
114
- <Link to="/account/plans">Change plan</Link>
115
- </Button>
116
- <Button variant="outline" asChild>
117
- <Link to="/account/plans">Billing settings</Link>
118
- </Button>
119
- {auth.isAuthenticated ? (
120
- <Button variant="secondary" asChild>
121
- <Link to={CREDENTIALS_PATH}>API credentials</Link>
122
- </Button>
123
- ) : null}
124
- </div>
125
- </div>
126
- );
127
- };
128
-
129
- export default SubscriptionsPage;
package/src/paths.ts DELETED
@@ -1,4 +0,0 @@
1
- export const PRICING_PATH = "/pricing";
2
- export const CHECKOUT_PATH = "/checkout";
3
- export const SUBSCRIPTIONS_PATH = "/account/subscriptions";
4
- export const CREDENTIALS_PATH = "/account/credentials";
package/src/types.ts DELETED
@@ -1,50 +0,0 @@
1
- export type DevPortalPlan = {
2
- sku: string;
3
- displayName: string;
4
- limitsJson: string | null;
5
- isFree: boolean;
6
- priceAmount: number;
7
- priceCurrency: string;
8
- billingPeriod: string;
9
- syncedAtUtc?: string;
10
- };
11
-
12
- export type DevPortalPlanCatalog = {
13
- plans: DevPortalPlan[];
14
- lastSyncedAtUtc?: string | null;
15
- };
16
-
17
- export type DevPortalSubscription = {
18
- planSku: string;
19
- status: string;
20
- entitlementState: string | null;
21
- currentPeriodEnd: string | null;
22
- };
23
-
24
- export type DevPortalMeResponse = {
25
- userId: string;
26
- displayName: string | null;
27
- subscriptions: DevPortalSubscription[];
28
- };
29
-
30
- export type CheckoutResponse = {
31
- checkoutUrl: string | null;
32
- isFree: boolean;
33
- entitlementProvisioned: boolean;
34
- };
35
-
36
- export type DevPortalCredentialsResponse = {
37
- idToken: string;
38
- accessToken: string | null;
39
- subscriptionKey: string;
40
- gatewayBaseUrl: string | null;
41
- planSku: string;
42
- expiresAt: string | null;
43
- };
44
-
45
- export type DevPortalBillingConfig = {
46
- pricing?: {
47
- title?: string;
48
- subtitle?: string;
49
- };
50
- };