@lukoweb/apitogo-plugin-dev-portal-billing 0.1.51 → 0.1.53

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.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import * as _lukoweb_apitogo0 from "@lukoweb/apitogo";
2
1
  import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as _lukoweb_apitogo_plugins0 from "@lukoweb/apitogo/plugins";
3
3
 
4
4
  //#region src/pages/AccountPlansPage.d.ts
5
5
  declare const AccountPlansPage: () => react_jsx_runtime0.JSX.Element;
@@ -9,7 +9,16 @@ type DevPortalBillingConfig = {
9
9
  pricing?: {
10
10
  title?: string;
11
11
  subtitle?: string;
12
- };
12
+ }; /** Local preview plan catalog (from `apitogo.config.json` `plans`). */
13
+ previewPlans?: Array<{
14
+ sku: string;
15
+ displayName: string;
16
+ priceAmount?: number;
17
+ priceCurrency?: string;
18
+ billingPeriod?: string;
19
+ limitsJson?: string | null;
20
+ includedActionKeys?: string[];
21
+ }>;
13
22
  };
14
23
  //#endregion
15
24
  //#region src/paths.d.ts
@@ -19,6 +28,6 @@ declare const SUBSCRIPTIONS_PATH = "/account/subscriptions";
19
28
  declare const CREDENTIALS_PATH = "/account/credentials";
20
29
  //#endregion
21
30
  //#region src/DevPortalBillingPlugin.d.ts
22
- declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo0.ApitogoPlugin;
31
+ declare const devPortalBillingPlugin: (options?: DevPortalBillingConfig | undefined) => _lukoweb_apitogo_plugins0.ApitogoPlugin;
23
32
  //#endregion
24
33
  export { AccountPlansPage, CHECKOUT_PATH, CREDENTIALS_PATH, PRICING_PATH, SUBSCRIPTIONS_PATH, devPortalBillingPlugin };
package/dist/index.mjs CHANGED
@@ -1,43 +1,71 @@
1
- import { Button, ClientOnly, Head, Heading } from "@lukoweb/apitogo/components";
1
+ import { Button, Head, Heading } from "@lukoweb/apitogo/components";
2
2
  import { CreditCardIcon, KeyRoundIcon, ShieldIcon, StarsIcon } from "@lukoweb/apitogo/icons";
3
3
  import { createContext, useContext, useEffect, useState } from "react";
4
- import { cn, createPlugin } from "@lukoweb/apitogo";
5
4
  import { Link, Navigate, Outlet, useNavigate, useSearchParams } from "@lukoweb/apitogo/router";
6
5
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@lukoweb/apitogo/ui/AlertDialog.js";
6
+ import { cn } from "@lukoweb/apitogo/ui/util.js";
7
7
  import manifestCatalog from "virtual:apitogo-local-manifest";
8
+ import zudokuConfig from "virtual:zudoku-config";
8
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
10
  import { useMutation, useQuery, useQueryClient } from "@lukoweb/apitogo/react-query";
10
11
  import { useAuth } from "@lukoweb/apitogo/hooks";
12
+ import { createPlugin } from "@lukoweb/apitogo/plugins";
11
13
  import { Alert, AlertAction, AlertDescription } from "@lukoweb/apitogo/ui/Alert";
12
14
  import { Button as Button$1 } from "@lukoweb/apitogo/ui/Button";
13
15
  import { Secret } from "@lukoweb/apitogo/ui/Secret.js";
14
16
  import { Value } from "@lukoweb/apitogo/ui/Value.js";
15
- import { isAuthenticationEnabled, readEffectiveManifest, resolveManifestEnv } from "@lukoweb/apitogo/local-manifest";
16
17
 
17
18
  //#region src/utils/dev-portal-api.ts
18
19
  const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
19
20
  function resolveDevPortalApiBaseUrl(configuredUrl) {
20
21
  const fromConfig = configuredUrl?.trim();
21
22
  if (fromConfig) return fromConfig.replace(/\/+$/, "");
22
- const envUrl = typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string" ? VITE_APITOGO_DEV_PORTAL_API_URL.trim() : "";
23
+ const envUrl = (typeof VITE_APITOGO_AUTHENTICATION_API_BASE_URL === "string" ? VITE_APITOGO_AUTHENTICATION_API_BASE_URL.trim() : "") || (typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string" ? VITE_APITOGO_DEV_PORTAL_API_URL.trim() : "");
23
24
  return envUrl ? envUrl.replace(/\/+$/, "") : void 0;
24
25
  }
25
26
  function isPlaceholderDevPortalApiUrl(apiBaseUrl) {
26
27
  if (!apiBaseUrl) return true;
27
28
  return apiBaseUrl === DEV_PORTAL_PLACEHOLDER_API_URL || apiBaseUrl.includes("dp-example.azurecontainerapps.io");
28
29
  }
29
- function parseManifestPlans() {
30
- if (!manifestCatalog?.plans?.length) return { plans: [] };
31
- return { plans: manifestCatalog.plans.map((plan) => ({
30
+ function mapPreviewPlans(plans) {
31
+ return plans.map((plan) => ({
32
32
  sku: plan.sku,
33
33
  displayName: plan.displayName,
34
- isFree: plan.isFree,
35
34
  priceAmount: plan.priceAmount,
36
35
  priceCurrency: plan.priceCurrency,
37
36
  billingPeriod: plan.billingPeriod,
38
37
  limitsJson: plan.limitsJson,
39
38
  includedActionKeys: plan.includedActionKeys
40
- })) };
39
+ }));
40
+ }
41
+ function readModulePlanItems() {
42
+ const items = (zudokuConfig?.modules)?.userPanel?.plans?.items;
43
+ return Array.isArray(items) && items.length > 0 ? items : void 0;
44
+ }
45
+ function parseManifestPlans(configPlans) {
46
+ if (configPlans?.length) return { plans: mapPreviewPlans(configPlans.map((plan) => ({
47
+ sku: plan.sku,
48
+ displayName: plan.displayName,
49
+ priceAmount: plan.priceAmount ?? 0,
50
+ priceCurrency: plan.priceCurrency ?? "usd",
51
+ billingPeriod: plan.billingPeriod ?? "monthly",
52
+ limitsJson: plan.limitsJson ?? null,
53
+ includedActionKeys: plan.includedActionKeys ?? []
54
+ }))) };
55
+ if (manifestCatalog?.plans?.length) return { plans: mapPreviewPlans(manifestCatalog.plans) };
56
+ const metaPlans = zudokuConfig?.__meta?.previewPlans;
57
+ if (metaPlans?.length) return { plans: mapPreviewPlans(metaPlans) };
58
+ const moduleItems = readModulePlanItems();
59
+ if (moduleItems?.length) return { plans: mapPreviewPlans(moduleItems.map((plan) => ({
60
+ sku: plan.sku,
61
+ displayName: plan.displayName,
62
+ priceAmount: plan.priceAmount ?? 0,
63
+ priceCurrency: plan.priceCurrency ?? "usd",
64
+ billingPeriod: plan.billingPeriod ?? "monthly",
65
+ limitsJson: plan.limitsJson ?? null,
66
+ includedActionKeys: plan.includedActionKeys ?? []
67
+ }))) };
68
+ return { plans: [] };
41
69
  }
42
70
  function formatActionKey(actionKey) {
43
71
  const separatorIndex = actionKey.indexOf(":");
@@ -45,7 +73,7 @@ function formatActionKey(actionKey) {
45
73
  return `${actionKey.slice(0, separatorIndex).toUpperCase()} ${actionKey.slice(separatorIndex + 1)}`;
46
74
  }
47
75
  function formatPlanPrice(plan) {
48
- if (plan.isFree || plan.priceAmount <= 0) return "Free";
76
+ if (plan.priceAmount <= 0) return "Free";
49
77
  const amount = new Intl.NumberFormat(void 0, {
50
78
  style: "currency",
51
79
  currency: plan.priceCurrency.toUpperCase(),
@@ -56,6 +84,9 @@ function formatPlanPrice(plan) {
56
84
  if (period === "annual" || period === "year") return `${amount}/yr`;
57
85
  return amount;
58
86
  }
87
+ function isPlanFree(plan) {
88
+ return plan.priceAmount <= 0;
89
+ }
59
90
  function formatBillingPeriod(period) {
60
91
  const normalized = period.toLowerCase();
61
92
  if (normalized === "month" || normalized === "monthly") return "Monthly";
@@ -131,7 +162,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
131
162
  children: formatActionKey(actionKey)
132
163
  }) }, actionKey))
133
164
  })]
134
- }) : plan.isFree ? /* @__PURE__ */ jsx("p", {
165
+ }) : isPlanFree(plan) ? /* @__PURE__ */ jsx("p", {
135
166
  className: "mt-4 text-sm text-muted-foreground",
136
167
  children: "API access not included"
137
168
  }) : null,
@@ -172,7 +203,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
172
203
  asChild: true,
173
204
  children: /* @__PURE__ */ jsx(Link, {
174
205
  to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
175
- children: plan.isFree ? "Get started" : "Subscribe"
206
+ children: isPlanFree(plan) ? "Get started" : "Subscribe"
176
207
  })
177
208
  }) : isActive ? /* @__PURE__ */ jsx(Button, {
178
209
  variant: "secondary",
@@ -188,7 +219,7 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
188
219
  asChild: true,
189
220
  children: /* @__PURE__ */ jsx(Link, {
190
221
  to: `/checkout?planSku=${encodeURIComponent(plan.sku)}`,
191
- children: plan.isFree ? "Get started" : "Subscribe"
222
+ children: isPlanFree(plan) ? "Get started" : "Subscribe"
192
223
  })
193
224
  })
194
225
  })
@@ -196,6 +227,15 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
196
227
  });
197
228
  };
198
229
 
230
+ //#endregion
231
+ //#region src/DevPortalBillingContext.tsx
232
+ const DevPortalBillingContext = createContext({});
233
+ const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
234
+ value: options,
235
+ children
236
+ });
237
+ const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
238
+
199
239
  //#endregion
200
240
  //#region src/hooks/useDevPortalApiBaseUrl.ts
201
241
  function useDevPortalApiBaseUrl() {
@@ -265,27 +305,45 @@ function useMySubscriptions() {
265
305
  function usePlans() {
266
306
  const apiBaseUrl = useDevPortalApiBaseUrl();
267
307
  const isPreview = useDevPortalPreviewMode();
268
- return useQuery({
269
- queryKey: [
270
- "dev-portal-plans",
271
- apiBaseUrl,
272
- isPreview
273
- ],
308
+ const { previewPlans } = useDevPortalBillingConfig();
309
+ const liveQuery = useQuery({
310
+ queryKey: ["dev-portal-plans", apiBaseUrl],
311
+ enabled: !isPreview && Boolean(apiBaseUrl),
274
312
  queryFn: async () => {
275
- if (isPreview) return parseManifestPlans();
276
313
  if (!apiBaseUrl) return { plans: [] };
277
314
  const response = await fetch(`${apiBaseUrl}/api/v1/plans`, { headers: { Accept: "application/json" } });
278
315
  if (!response.ok) throw new Error("Failed to load plans.");
279
316
  return await response.json();
280
317
  }
281
318
  });
319
+ if (isPreview) {
320
+ const data = parseManifestPlans(previewPlans);
321
+ return {
322
+ ...liveQuery,
323
+ data,
324
+ dataUpdatedAt: Date.now(),
325
+ error: null,
326
+ fetchStatus: "idle",
327
+ isError: false,
328
+ isFetching: false,
329
+ isLoading: false,
330
+ isPending: false,
331
+ isSuccess: true,
332
+ status: "success"
333
+ };
334
+ }
335
+ return liveQuery;
282
336
  }
283
337
 
284
338
  //#endregion
285
339
  //#region src/pages/AccountPlansPage.tsx
286
340
  const AccountPlansPage = () => {
341
+ const { previewPlans } = useDevPortalBillingConfig();
287
342
  const isPreview = useDevPortalPreviewMode();
288
- const { data: catalog, isLoading, isError } = usePlans();
343
+ const livePlans = usePlans();
344
+ const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
345
+ const isLoading = isPreview ? false : livePlans.isLoading;
346
+ const isError = isPreview ? false : livePlans.isError;
289
347
  const { data: me } = useMySubscriptions();
290
348
  const cancelMutation = useCancelSubscription();
291
349
  const [cancelingSku, setCancelingSku] = useState(null);
@@ -372,20 +430,11 @@ const AccountPlansPage = () => {
372
430
  });
373
431
  };
374
432
 
375
- //#endregion
376
- //#region src/DevPortalBillingContext.tsx
377
- const DevPortalBillingContext = createContext({});
378
- const DevPortalBillingProvider = ({ options, children }) => /* @__PURE__ */ jsx(DevPortalBillingContext.Provider, {
379
- value: options,
380
- children
381
- });
382
- const useDevPortalBillingConfig = () => useContext(DevPortalBillingContext);
383
-
384
433
  //#endregion
385
434
  //#region src/DevPortalBillingWrapper.tsx
386
435
  const DevPortalBillingWrapper = ({ options = {} }) => /* @__PURE__ */ jsx(DevPortalBillingProvider, {
387
436
  options,
388
- children: /* @__PURE__ */ jsx(ClientOnly, { children: /* @__PURE__ */ jsx(Outlet, {}) })
437
+ children: /* @__PURE__ */ jsx(Outlet, {})
389
438
  });
390
439
 
391
440
  //#endregion
@@ -436,6 +485,7 @@ const PRICING_PATH = "/pricing";
436
485
  const CHECKOUT_PATH = "/checkout";
437
486
  const SUBSCRIPTIONS_PATH = "/account/subscriptions";
438
487
  const CREDENTIALS_PATH = "/account/credentials";
488
+ const LOGIN_NAV_PATH = "/signin";
439
489
 
440
490
  //#endregion
441
491
  //#region src/pages/CheckoutPage.tsx
@@ -474,7 +524,7 @@ const CheckoutPage = () => {
474
524
  return await response.json();
475
525
  }
476
526
  });
477
- const freeProvisioned = checkoutQuery.data?.isFree === true && checkoutQuery.data.entitlementProvisioned === true;
527
+ const freeProvisioned = checkoutQuery.data?.checkoutUrl == null && checkoutQuery.data?.entitlementProvisioned === true;
478
528
  useEffect(() => {
479
529
  if (freeProvisioned) navigate("/account/subscriptions?checkout=success", { replace: true });
480
530
  }, [freeProvisioned, navigate]);
@@ -716,23 +766,27 @@ const CredentialsPage = () => {
716
766
  //#endregion
717
767
  //#region src/utils/pricing-config.ts
718
768
  function isPreviewPricingEnabled() {
719
- return manifestCatalog?.pricingEnabled === true;
769
+ if (manifestCatalog?.pricingEnabled === true) return true;
770
+ return zudokuConfig?.__meta?.pricingEnabled === true;
720
771
  }
721
772
 
722
773
  //#endregion
723
774
  //#region src/pages/PricingPage.tsx
724
775
  const PricingPage = () => {
725
- const { pricing } = useDevPortalBillingConfig();
776
+ const { pricing, previewPlans } = useDevPortalBillingConfig();
726
777
  const auth = useAuth();
727
778
  const isPreview = useDevPortalPreviewMode();
728
- const pricingEnabled = isPreviewPricingEnabled();
729
- const { data: catalog, isLoading, isError } = usePlans();
779
+ const pricingEnabled = isPreviewPricingEnabled() || isPreview;
780
+ const livePlans = usePlans();
781
+ const catalog = isPreview ? parseManifestPlans(previewPlans) : livePlans.data;
782
+ const isLoading = isPreview ? false : livePlans.isLoading;
783
+ const isError = isPreview ? false : livePlans.isError;
730
784
  const { data: me } = useMySubscriptions();
731
785
  if (!pricingEnabled) return /* @__PURE__ */ jsx(Navigate, {
732
786
  to: "/",
733
787
  replace: true
734
788
  });
735
- if (auth.isAuthenticated) return /* @__PURE__ */ jsx(Navigate, {
789
+ if (auth.sessionChecked && auth.isAuthenticated && !isPreview) return /* @__PURE__ */ jsx(Navigate, {
736
790
  to: "/account/plans",
737
791
  replace: true
738
792
  });
@@ -774,8 +828,8 @@ const PricingPage = () => {
774
828
  className: "rounded bg-muted px-1.5 py-0.5",
775
829
  children: ".env"
776
830
  }),
831
+ " with",
777
832
  " ",
778
- "with ",
779
833
  /* @__PURE__ */ jsx("code", {
780
834
  className: "rounded bg-muted px-1.5 py-0.5",
781
835
  children: "APITOGO_CONFIG_*"
@@ -812,7 +866,7 @@ const PricingPage = () => {
812
866
  " ",
813
867
  /* @__PURE__ */ jsx("code", {
814
868
  className: "rounded bg-muted px-1.5 py-0.5",
815
- children: "pricing.enabled"
869
+ children: "modules.userPanel.plans.enabled"
816
870
  }),
817
871
  " ",
818
872
  "to true."
@@ -946,11 +1000,9 @@ const SubscriptionsPage = () => {
946
1000
 
947
1001
  //#endregion
948
1002
  //#region src/utils/auth-config.ts
949
- const LOGIN_NAV_PATH = "/signin";
950
1003
  async function resolveAuthenticationNavEnabled(config) {
951
1004
  if (config.__meta?.authenticationEnabled !== void 0) return config.__meta.authenticationEnabled;
952
- if (manifestCatalog?.authenticationEnabled === true) return true;
953
- return isAuthenticationEnabled(await readEffectiveManifest(config.__meta?.rootDir ?? process.cwd(), resolveManifestEnv(process.env.NODE_ENV === "production" ? "production" : "development")));
1005
+ return manifestCatalog?.authenticationEnabled === true;
954
1006
  }
955
1007
 
956
1008
  //#endregion
@@ -973,6 +1025,18 @@ function withoutLoginNavigation(items) {
973
1025
 
974
1026
  //#endregion
975
1027
  //#region src/utils/navigation-plans.ts
1028
+ function hasPricingLink(items) {
1029
+ return items.some((item) => typeof item !== "string" && item.type === "link" && item.to === PRICING_PATH);
1030
+ }
1031
+ function withPricingNavigation(items) {
1032
+ const current = items ?? [];
1033
+ if (hasPricingLink(current)) return current;
1034
+ return [...current, {
1035
+ type: "link",
1036
+ to: PRICING_PATH,
1037
+ label: "Pricing"
1038
+ }];
1039
+ }
976
1040
  function withoutPricingNavigation(items) {
977
1041
  if (!items?.length) return [];
978
1042
  return items.map((item) => {
@@ -1027,7 +1091,7 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
1027
1091
  authNavEnabled
1028
1092
  });
1029
1093
  return merge({
1030
- navigation: pricingEnabled ? config.navigation : withoutPricingNavigation(config.navigation),
1094
+ navigation: pricingEnabled ? withPricingNavigation(config.navigation) : withoutPricingNavigation(config.navigation),
1031
1095
  header: {
1032
1096
  ...config.header,
1033
1097
  navigation: headerNavigation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,7 +46,7 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.1.51"
49
+ "@lukoweb/apitogo": "0.1.53"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": ">=19.2.0",