@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 CHANGED
@@ -10,6 +10,7 @@ import { useAuth } from "@lukoweb/apitogo/hooks";
10
10
  import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
11
11
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
12
12
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
13
+ import { Value } from "@lukoweb/apitogo/ui/Value.js";
13
14
 
14
15
  //#region src/utils/dev-portal-api.ts
15
16
  const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
@@ -645,11 +646,9 @@ const CredentialsPage = () => {
645
646
  className: "text-sm text-muted-foreground",
646
647
  children: "Base URL for API requests through the gateway."
647
648
  })]
648
- }), credentials.gatewayBaseUrl ? /* @__PURE__ */ jsx(Secret, {
649
+ }), credentials.gatewayBaseUrl ? /* @__PURE__ */ jsx(Value, {
649
650
  className: "max-w-full w-full",
650
- secret: credentials.gatewayBaseUrl,
651
- status: "none",
652
- noReveal: true
651
+ value: credentials.gatewayBaseUrl
653
652
  }) : /* @__PURE__ */ jsx("p", {
654
653
  className: "text-sm text-muted-foreground",
655
654
  children: "Available after your API is deployed to the gateway."
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
7
7
  "directory": "packages/plugin-dev-portal-billing"
8
8
  },
9
9
  "type": "module",
10
- "main": "./src/index.ts",
11
- "types": "./src/index.ts",
10
+ "main": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts",
12
12
  "exports": {
13
- ".": "./src/index.ts"
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts"
16
+ }
14
17
  },
15
18
  "files": [
16
- "dist",
17
- "src"
19
+ "dist"
18
20
  ],
19
21
  "scripts": {
20
22
  "build": "tsdown",
@@ -44,7 +46,7 @@
44
46
  "react": "19.2.4",
45
47
  "react-dom": "19.2.4",
46
48
  "tsdown": "0.20.3",
47
- "@lukoweb/apitogo": "0.1.41"
49
+ "@lukoweb/apitogo": "0.1.43"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "react": ">=19.2.0",
@@ -1,19 +0,0 @@
1
- import { createContext, useContext } from "react";
2
- import type { DevPortalBillingConfig } from "./types.js";
3
-
4
- const DevPortalBillingContext = createContext<DevPortalBillingConfig>({});
5
-
6
- export const DevPortalBillingProvider = ({
7
- options,
8
- children,
9
- }: {
10
- options: DevPortalBillingConfig;
11
- children: React.ReactNode;
12
- }) => (
13
- <DevPortalBillingContext.Provider value={options}>
14
- {children}
15
- </DevPortalBillingContext.Provider>
16
- );
17
-
18
- export const useDevPortalBillingConfig = () =>
19
- useContext(DevPortalBillingContext);
@@ -1,29 +0,0 @@
1
- import { StaticZudoku } from "@lukoweb/apitogo/testing";
2
- import { act, render, screen } from "@testing-library/react";
3
- import { describe, expect, it } from "vitest";
4
- import { devPortalBillingPlugin } from "./DevPortalBillingPlugin.js";
5
-
6
- describe("PricingPage", () => {
7
- it("renders pricing page with custom title", async () => {
8
- await act(async () => {
9
- render(
10
- <StaticZudoku
11
- plugins={[
12
- devPortalBillingPlugin({
13
- pricing: {
14
- title: "API Plans",
15
- subtitle: "Choose the plan that fits your integration.",
16
- },
17
- }),
18
- ]}
19
- path="/pricing"
20
- />,
21
- );
22
- });
23
-
24
- expect(screen.getByTestId("title")).toHaveTextContent("API Plans");
25
- expect(screen.getByTestId("subtitle")).toHaveTextContent(
26
- "Choose the plan that fits your integration.",
27
- );
28
- });
29
- });
@@ -1,87 +0,0 @@
1
- import { createPlugin } from "@lukoweb/apitogo";
2
- import { KeyRoundIcon, StarsIcon } from "@lukoweb/apitogo/icons";
3
- import DevPortalBillingWrapper from "./DevPortalBillingWrapper.js";
4
- import CheckoutPage from "./pages/CheckoutPage.js";
5
- import CredentialsPage from "./pages/CredentialsPage.js";
6
- import PricingPage from "./pages/PricingPage.js";
7
- import SubscriptionsPage from "./pages/SubscriptionsPage.js";
8
- import {
9
- CHECKOUT_PATH,
10
- CREDENTIALS_PATH,
11
- PRICING_PATH,
12
- SUBSCRIPTIONS_PATH,
13
- } from "./paths.js";
14
- import type { DevPortalBillingConfig } from "./types.js";
15
-
16
- export {
17
- CHECKOUT_PATH,
18
- CREDENTIALS_PATH,
19
- PRICING_PATH,
20
- SUBSCRIPTIONS_PATH,
21
- } from "./paths.js";
22
-
23
- export const devPortalBillingPlugin = createPlugin(
24
- (options: DevPortalBillingConfig = {}) => ({
25
- transformConfig: ({ config, merge }) =>
26
- merge({
27
- header: {
28
- navigation: [
29
- ...(config.header?.navigation ?? []),
30
- { label: "Pricing", to: PRICING_PATH, display: "anon" },
31
- ],
32
- },
33
- }),
34
-
35
- getProfileMenuItems: () => [
36
- {
37
- label: "My Subscriptions",
38
- path: SUBSCRIPTIONS_PATH,
39
- icon: StarsIcon,
40
- category: "middle",
41
- weight: 25,
42
- },
43
- {
44
- label: "API credentials",
45
- path: CREDENTIALS_PATH,
46
- icon: KeyRoundIcon,
47
- category: "middle",
48
- weight: 26,
49
- },
50
- ],
51
-
52
- getRoutes: () => [
53
- {
54
- element: <DevPortalBillingWrapper options={options} />,
55
- handle: { layout: "none" },
56
- children: [
57
- {
58
- path: PRICING_PATH,
59
- handle: { layout: "default" },
60
- element: <PricingPage />,
61
- },
62
- {
63
- path: CHECKOUT_PATH,
64
- handle: { layout: "default" },
65
- element: <CheckoutPage />,
66
- },
67
- {
68
- path: SUBSCRIPTIONS_PATH,
69
- handle: { layout: "default" },
70
- element: <SubscriptionsPage />,
71
- },
72
- {
73
- path: CREDENTIALS_PATH,
74
- handle: { layout: "default" },
75
- element: <CredentialsPage />,
76
- },
77
- ],
78
- },
79
- ],
80
-
81
- getProtectedRoutes: () => [
82
- CHECKOUT_PATH,
83
- SUBSCRIPTIONS_PATH,
84
- CREDENTIALS_PATH,
85
- ],
86
- }),
87
- );
@@ -1,18 +0,0 @@
1
- import { ClientOnly } from "@lukoweb/apitogo/components";
2
- import { Outlet } from "@lukoweb/apitogo/router";
3
- import { DevPortalBillingProvider } from "./DevPortalBillingContext.js";
4
- import type { DevPortalBillingConfig } from "./types.js";
5
-
6
- export const DevPortalBillingWrapper = ({
7
- options = {},
8
- }: {
9
- options: DevPortalBillingConfig;
10
- }) => (
11
- <DevPortalBillingProvider options={options}>
12
- <ClientOnly>
13
- <Outlet />
14
- </ClientOnly>
15
- </DevPortalBillingProvider>
16
- );
17
-
18
- export default DevPortalBillingWrapper;
@@ -1,47 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import { describe, expect, it, vi } from "vitest";
3
- import type { DevPortalPlan } from "../types.js";
4
- import { PricingCard } from "./PricingCard.js";
5
-
6
- const plan: DevPortalPlan = {
7
- sku: "pro-monthly",
8
- displayName: "Pro",
9
- limitsJson: null,
10
- isFree: false,
11
- priceAmount: 29,
12
- priceCurrency: "usd",
13
- billingPeriod: "monthly",
14
- };
15
-
16
- vi.mock("@lukoweb/apitogo/router", () => ({
17
- Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
18
- <a href={to}>{children}</a>
19
- ),
20
- }));
21
-
22
- describe("PricingCard", () => {
23
- it("shows unsubscribe in account mode for active subscriptions", () => {
24
- render(
25
- <PricingCard
26
- plan={plan}
27
- mode="account"
28
- isSubscribed
29
- onUnsubscribe={vi.fn()}
30
- />,
31
- );
32
-
33
- expect(screen.getByTestId("subscribed-badge-pro-monthly")).toBeTruthy();
34
- expect(screen.getByTestId("unsubscribe-pro-monthly")).toBeTruthy();
35
- expect(screen.getByText("Current plan")).toBeTruthy();
36
- });
37
-
38
- it("shows subscribe in account mode for unsubscribed plans", () => {
39
- render(<PricingCard plan={plan} mode="account" isSubscribed={false} />);
40
-
41
- expect(screen.queryByTestId("subscribed-badge-pro-monthly")).toBeNull();
42
- expect(screen.getByRole("link", { name: "Subscribe" })).toHaveAttribute(
43
- "href",
44
- "/checkout?planSku=pro-monthly",
45
- );
46
- });
47
- });
@@ -1,142 +0,0 @@
1
- import { cn } from "@lukoweb/apitogo";
2
- import { Button } from "@lukoweb/apitogo/components";
3
- import { Link } from "@lukoweb/apitogo/router";
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
13
- AlertDialogTrigger,
14
- } from "@lukoweb/apitogo/ui/AlertDialog.js";
15
- import type { DevPortalPlan } from "../types.js";
16
- import {
17
- formatBillingPeriod,
18
- formatPlanPrice,
19
- isActiveSubscription,
20
- summarizeLimits,
21
- } from "../utils/dev-portal-api.js";
22
-
23
- export type PricingCardMode = "public" | "account";
24
-
25
- export const PricingCard = ({
26
- plan,
27
- mode = "public",
28
- subscriptionStatus,
29
- isSubscribed = false,
30
- onUnsubscribe,
31
- isCanceling = false,
32
- cancelDisabled = false,
33
- }: {
34
- plan: DevPortalPlan;
35
- mode?: PricingCardMode;
36
- subscriptionStatus?: string;
37
- isSubscribed?: boolean;
38
- onUnsubscribe?: (planSku: string) => void;
39
- isCanceling?: boolean;
40
- cancelDisabled?: boolean;
41
- }) => {
42
- const limitsSummary = summarizeLimits(plan.limitsJson);
43
- const isActive =
44
- isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
45
-
46
- return (
47
- <article
48
- className={cn(
49
- "flex w-full max-w-sm flex-col rounded-xl border bg-card/60 p-6 shadow-xs",
50
- isActive &&
51
- mode === "account" &&
52
- "border-primary/40 ring-1 ring-primary/20",
53
- )}
54
- data-testid={`pricing-card-${plan.sku}`}
55
- >
56
- <div className="flex items-start justify-between gap-2">
57
- <h2 className="text-xl font-semibold">{plan.displayName}</h2>
58
- {isActive && mode === "account" ? (
59
- <span
60
- className="shrink-0 rounded-full bg-primary/10 px-3 py-1 text-xs font-medium text-primary"
61
- data-testid={`subscribed-badge-${plan.sku}`}
62
- >
63
- Subscribed
64
- </span>
65
- ) : null}
66
- </div>
67
- <p className="mt-2 text-3xl font-bold">{formatPlanPrice(plan)}</p>
68
- <p className="mt-1 text-sm text-muted-foreground">
69
- {formatBillingPeriod(plan.billingPeriod)}
70
- </p>
71
- {limitsSummary ? (
72
- <p className="mt-4 text-sm text-muted-foreground">{limitsSummary}</p>
73
- ) : null}
74
- <div className="mt-6 space-y-2">
75
- {mode === "account" ? (
76
- isActive ? (
77
- <>
78
- <Button variant="secondary" className="w-full" disabled>
79
- Current plan
80
- </Button>
81
- {onUnsubscribe ? (
82
- <AlertDialog>
83
- <AlertDialogTrigger asChild>
84
- <Button
85
- variant="outline"
86
- className="w-full"
87
- disabled={cancelDisabled || isCanceling}
88
- data-testid={`unsubscribe-${plan.sku}`}
89
- >
90
- {isCanceling ? "Canceling…" : "Unsubscribe"}
91
- </Button>
92
- </AlertDialogTrigger>
93
- <AlertDialogContent>
94
- <AlertDialogHeader>
95
- <AlertDialogTitle>
96
- Unsubscribe from {plan.displayName}?
97
- </AlertDialogTitle>
98
- <AlertDialogDescription>
99
- Your API access for this plan will be revoked. You can
100
- subscribe again later.
101
- </AlertDialogDescription>
102
- </AlertDialogHeader>
103
- <AlertDialogFooter>
104
- <AlertDialogCancel>Keep subscription</AlertDialogCancel>
105
- <AlertDialogAction
106
- onClick={() => onUnsubscribe(plan.sku)}
107
- disabled={isCanceling}
108
- >
109
- Unsubscribe
110
- </AlertDialogAction>
111
- </AlertDialogFooter>
112
- </AlertDialogContent>
113
- </AlertDialog>
114
- ) : null}
115
- {cancelDisabled ? (
116
- <p className="text-center text-xs text-muted-foreground">
117
- Unsubscribe is available after the portal is published.
118
- </p>
119
- ) : null}
120
- </>
121
- ) : (
122
- <Button variant="default" className="w-full" asChild>
123
- <Link to={`/checkout?planSku=${encodeURIComponent(plan.sku)}`}>
124
- {plan.isFree ? "Get started" : "Subscribe"}
125
- </Link>
126
- </Button>
127
- )
128
- ) : isActive ? (
129
- <Button variant="secondary" className="w-full" asChild>
130
- <Link to="/account/subscriptions">Manage subscription</Link>
131
- </Button>
132
- ) : (
133
- <Button variant="default" className="w-full" asChild>
134
- <Link to={`/checkout?planSku=${encodeURIComponent(plan.sku)}`}>
135
- {plan.isFree ? "Get started" : "Subscribe"}
136
- </Link>
137
- </Button>
138
- )}
139
- </div>
140
- </article>
141
- );
142
- };
@@ -1,47 +0,0 @@
1
- import type { LucideIcon } from "@lukoweb/apitogo/icons";
2
- import { useEffect } from "react";
3
-
4
- export const RedirectPage = ({
5
- icon: Icon,
6
- title,
7
- description,
8
- url,
9
- children,
10
- }: {
11
- icon: LucideIcon;
12
- title: string;
13
- description: string;
14
- url?: string;
15
- children?: React.ReactNode;
16
- }) => {
17
- useEffect(() => {
18
- if (url) {
19
- window.location.assign(url);
20
- }
21
- }, [url]);
22
-
23
- return (
24
- <div className="flex min-h-screen items-center justify-center bg-muted">
25
- <div className="flex max-w-md flex-col items-center space-y-6 text-center">
26
- <div className="relative">
27
- <div className="flex h-24 w-24 items-center justify-center rounded-full bg-foreground/10">
28
- <Icon className="h-12 w-12 text-foreground" />
29
- </div>
30
- </div>
31
-
32
- {children}
33
-
34
- <div className="space-y-2">
35
- <h2 className="text-2xl font-bold text-card-foreground">{title}</h2>
36
- <p className="text-muted-foreground">{description}</p>
37
- </div>
38
-
39
- <div className="flex space-x-2">
40
- <div className="h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.3s]" />
41
- <div className="h-3 w-3 animate-pulse rounded-full bg-primary [animation-delay:-0.15s]" />
42
- <div className="h-3 w-3 animate-pulse rounded-full bg-primary" />
43
- </div>
44
- </div>
45
- </div>
46
- );
47
- };
package/src/env.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const VITE_APITOGO_DEV_PORTAL_API_URL: string | undefined;
2
- declare const VITE_APITOGO_DEV_PORTAL_MOCK_PLANS: string | undefined;
@@ -1,44 +0,0 @@
1
- import { useMutation, useQueryClient } from "@lukoweb/apitogo/react-query";
2
- import { useDevPortalApiBaseUrl } from "./useDevPortalApiBaseUrl.js";
3
-
4
- type CancelSubscriptionResponse = {
5
- canceled: boolean;
6
- };
7
-
8
- export function useCancelSubscription() {
9
- const apiBaseUrl = useDevPortalApiBaseUrl();
10
- const queryClient = useQueryClient();
11
-
12
- return useMutation({
13
- mutationFn: async (planSku: string) => {
14
- if (!apiBaseUrl) {
15
- throw new Error("Dev portal API is not configured.");
16
- }
17
-
18
- const response = await fetch(
19
- `${apiBaseUrl}/api/v1/subscriptions/cancel`,
20
- {
21
- method: "POST",
22
- headers: {
23
- Accept: "application/json",
24
- "Content-Type": "application/json",
25
- },
26
- credentials: "include",
27
- body: JSON.stringify({ planSku }),
28
- },
29
- );
30
-
31
- if (!response.ok) {
32
- const detail = await response.text();
33
- throw new Error(detail || "Failed to cancel subscription.");
34
- }
35
-
36
- return (await response.json()) as CancelSubscriptionResponse;
37
- },
38
- onSuccess: async () => {
39
- await queryClient.invalidateQueries({
40
- queryKey: ["dev-portal-me", apiBaseUrl],
41
- });
42
- },
43
- });
44
- }
@@ -1,39 +0,0 @@
1
- import { useAuth } from "@lukoweb/apitogo/hooks";
2
- import { useQuery } from "@lukoweb/apitogo/react-query";
3
- import type { DevPortalCredentialsResponse } from "../types.js";
4
- import {
5
- useDevPortalApiBaseUrl,
6
- useDevPortalPreviewMode,
7
- } from "./useDevPortalApiBaseUrl.js";
8
-
9
- export function useCredentials() {
10
- const auth = useAuth();
11
- const apiBaseUrl = useDevPortalApiBaseUrl();
12
- const isPreview = useDevPortalPreviewMode();
13
-
14
- return useQuery<DevPortalCredentialsResponse>({
15
- queryKey: ["dev-portal-credentials", apiBaseUrl],
16
- enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
17
- queryFn: async () => {
18
- const response = await fetch(`${apiBaseUrl}/api/v1/credentials`, {
19
- method: "GET",
20
- headers: { Accept: "application/json" },
21
- credentials: "include",
22
- });
23
-
24
- if (response.status === 404) {
25
- throw new Error("NO_ENTITLEMENT");
26
- }
27
-
28
- if (!response.ok) {
29
- throw new Error("Failed to load API credentials.");
30
- }
31
-
32
- return (await response.json()) as DevPortalCredentialsResponse;
33
- },
34
- retry: (failureCount, error) =>
35
- error instanceof Error && error.message === "NO_ENTITLEMENT"
36
- ? false
37
- : failureCount < 2,
38
- });
39
- }
@@ -1,33 +0,0 @@
1
- import { useAuth } from "@lukoweb/apitogo/hooks";
2
- import {
3
- isPlaceholderDevPortalApiUrl,
4
- resolveDevPortalApiBaseUrl,
5
- } from "../utils/dev-portal-api.js";
6
-
7
- type DevPortalProviderData = {
8
- type: "dev-portal";
9
- apiBaseUrl: string;
10
- authMode: "live" | "preview";
11
- };
12
-
13
- export function useDevPortalApiBaseUrl(): string | undefined {
14
- const auth = useAuth();
15
- const providerData = auth.providerData as DevPortalProviderData | null;
16
-
17
- if (providerData?.type === "dev-portal" && providerData.apiBaseUrl) {
18
- return providerData.apiBaseUrl;
19
- }
20
-
21
- return resolveDevPortalApiBaseUrl();
22
- }
23
-
24
- export function useDevPortalPreviewMode(): boolean {
25
- const auth = useAuth();
26
- const apiBaseUrl = useDevPortalApiBaseUrl();
27
-
28
- if (auth.authMode === "preview" || auth.isBackendAvailable === false) {
29
- return true;
30
- }
31
-
32
- return isPlaceholderDevPortalApiUrl(apiBaseUrl);
33
- }
@@ -1,31 +0,0 @@
1
- import { useAuth } from "@lukoweb/apitogo/hooks";
2
- import { useQuery } from "@lukoweb/apitogo/react-query";
3
- import type { DevPortalMeResponse } from "../types.js";
4
- import {
5
- useDevPortalApiBaseUrl,
6
- useDevPortalPreviewMode,
7
- } from "./useDevPortalApiBaseUrl.js";
8
-
9
- export function useMySubscriptions() {
10
- const auth = useAuth();
11
- const apiBaseUrl = useDevPortalApiBaseUrl();
12
- const isPreview = useDevPortalPreviewMode();
13
-
14
- return useQuery<DevPortalMeResponse>({
15
- queryKey: ["dev-portal-me", apiBaseUrl],
16
- enabled: auth.isAuthenticated && !isPreview && Boolean(apiBaseUrl),
17
- queryFn: async () => {
18
- const response = await fetch(`${apiBaseUrl}/api/v1/auth/me`, {
19
- method: "GET",
20
- headers: { Accept: "application/json" },
21
- credentials: "include",
22
- });
23
-
24
- if (!response.ok) {
25
- throw new Error("Failed to load subscription status.");
26
- }
27
-
28
- return (await response.json()) as DevPortalMeResponse;
29
- },
30
- });
31
- }
@@ -1,35 +0,0 @@
1
- import { useQuery } from "@lukoweb/apitogo/react-query";
2
- import type { DevPortalPlanCatalog } from "../types.js";
3
- import { parseMockPlans } from "../utils/dev-portal-api.js";
4
- import {
5
- useDevPortalApiBaseUrl,
6
- useDevPortalPreviewMode,
7
- } from "./useDevPortalApiBaseUrl.js";
8
-
9
- export function usePlans() {
10
- const apiBaseUrl = useDevPortalApiBaseUrl();
11
- const isPreview = useDevPortalPreviewMode();
12
-
13
- return useQuery<DevPortalPlanCatalog>({
14
- queryKey: ["dev-portal-plans", apiBaseUrl, isPreview],
15
- queryFn: async () => {
16
- if (isPreview) {
17
- return parseMockPlans();
18
- }
19
-
20
- if (!apiBaseUrl) {
21
- return { plans: [] };
22
- }
23
-
24
- const response = await fetch(`${apiBaseUrl}/api/v1/plans`, {
25
- headers: { Accept: "application/json" },
26
- });
27
-
28
- if (!response.ok) {
29
- throw new Error("Failed to load plans.");
30
- }
31
-
32
- return (await response.json()) as DevPortalPlanCatalog;
33
- },
34
- });
35
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- export { default as AccountPlansPage } from "./pages/AccountPlansPage.js";
2
- export { devPortalBillingPlugin } from "./DevPortalBillingPlugin.js";
3
- export {
4
- CHECKOUT_PATH,
5
- CREDENTIALS_PATH,
6
- PRICING_PATH,
7
- SUBSCRIPTIONS_PATH,
8
- } from "./paths.js";