@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.
- package/dist/index.mjs +3 -4
- package/package.json +9 -7
- package/src/DevPortalBillingContext.tsx +0 -19
- package/src/DevPortalBillingPlugin.test.tsx +0 -29
- package/src/DevPortalBillingPlugin.tsx +0 -87
- package/src/DevPortalBillingWrapper.tsx +0 -18
- package/src/components/PricingCard.test.tsx +0 -47
- package/src/components/PricingCard.tsx +0 -142
- package/src/components/RedirectPage.tsx +0 -47
- package/src/env.d.ts +0 -2
- package/src/hooks/useCancelSubscription.ts +0 -44
- package/src/hooks/useCredentials.ts +0 -39
- package/src/hooks/useDevPortalApiBaseUrl.ts +0 -33
- package/src/hooks/useMySubscriptions.ts +0 -31
- package/src/hooks/usePlans.ts +0 -35
- package/src/index.ts +0 -8
- package/src/pages/AccountPlansPage.tsx +0 -106
- package/src/pages/CheckoutPage.tsx +0 -127
- package/src/pages/CredentialsPage.tsx +0 -145
- package/src/pages/PricingPage.tsx +0 -87
- package/src/pages/SubscriptionsPage.tsx +0 -129
- package/src/paths.ts +0 -4
- package/src/types.ts +0 -50
- package/src/utils/dev-portal-api.test.ts +0 -89
- package/src/utils/dev-portal-api.ts +0 -156
|
@@ -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
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
|
-
};
|