@lukoweb/apitogo-plugin-dev-portal-billing 0.2.24 → 0.2.29

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.
Files changed (2) hide show
  1. package/dist/index.mjs +92 -2
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -231,7 +231,7 @@ const accountCtaLabel = (plan) => {
231
231
  };
232
232
  /** Custom-priced plans skip checkout and open the contact-sales form. */
233
233
  const planCtaPath = (plan) => `${isPlanCustomPriced(plan) ? CONTACT_SALES_PATH : CHECKOUT_PATH}?planSku=${encodeURIComponent(plan.sku)}`;
234
- const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, isCanceling = false, cancelDisabled = false, featured = false }) => {
234
+ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed = false, onUnsubscribe, onSwitch, isSwitching = false, isCanceling = false, cancelDisabled = false, featured = false }) => {
235
235
  const isActive = isSubscribed || isActiveSubscription(subscriptionStatus ?? "");
236
236
  const features = listPlanFeatureLines(plan);
237
237
  const displayPrice = formatPlanDisplayPrice(plan);
@@ -296,7 +296,31 @@ const PricingCard = ({ plan, mode = "public", subscriptionStatus, isSubscribed =
296
296
  className: "text-center text-xs text-muted-foreground",
297
297
  children: "Unsubscribe is available after the portal is published."
298
298
  }) : null
299
- ] }) : /* @__PURE__ */ jsx(Button, {
299
+ ] }) : onSwitch && !isPlanCustomPriced(plan) ? /* @__PURE__ */ jsxs(AlertDialog, { children: [/* @__PURE__ */ jsx(AlertDialogTrigger, {
300
+ asChild: true,
301
+ children: /* @__PURE__ */ jsx(Button, {
302
+ variant: featured ? "default" : "outline",
303
+ className: "w-full",
304
+ disabled: isSwitching,
305
+ "data-testid": `switch-${plan.sku}`,
306
+ children: isSwitching ? "Switching…" : "Switch to this plan"
307
+ })
308
+ }), /* @__PURE__ */ jsxs(AlertDialogContent, { children: [/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
309
+ "Switch to ",
310
+ plan.displayName,
311
+ "?"
312
+ ] }), /* @__PURE__ */ jsxs(AlertDialogDescription, { children: [
313
+ "Your current plan is replaced by ",
314
+ plan.displayName,
315
+ ". The API keys of your current plan stop working. Create a key for",
316
+ " ",
317
+ plan.displayName,
318
+ " on the API keys page."
319
+ ] })] }), /* @__PURE__ */ jsxs(AlertDialogFooter, { children: [/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Keep current plan" }), /* @__PURE__ */ jsx(AlertDialogAction, {
320
+ onClick: () => onSwitch(plan.sku),
321
+ disabled: isSwitching,
322
+ children: "Switch plan"
323
+ })] })] })] }) : /* @__PURE__ */ jsx(Button, {
300
324
  variant: featured ? "default" : "outline",
301
325
  className: "w-full",
302
326
  asChild: true,
@@ -386,6 +410,42 @@ function useCancelSubscription() {
386
410
  });
387
411
  }
388
412
 
413
+ //#endregion
414
+ //#region src/hooks/useChangePlan.ts
415
+ function useChangePlan() {
416
+ const apiBaseUrl = useDevPortalApiBaseUrl();
417
+ const queryClient = useQueryClient();
418
+ return useMutation({
419
+ mutationFn: async ({ fromPlanSku, toPlanSku }) => {
420
+ if (!apiBaseUrl) throw new Error("Dev portal API is not configured.");
421
+ const response = await fetch(`${apiBaseUrl}/api/v1/subscriptions/change`, {
422
+ method: "POST",
423
+ headers: {
424
+ Accept: "application/json",
425
+ "Content-Type": "application/json"
426
+ },
427
+ credentials: "include",
428
+ body: JSON.stringify({
429
+ fromPlanSku,
430
+ toPlanSku
431
+ })
432
+ });
433
+ if (!response.ok) {
434
+ const detail = await response.text();
435
+ throw new Error(detail || "Failed to change plan.");
436
+ }
437
+ return await response.json();
438
+ },
439
+ onSuccess: async (result) => {
440
+ if (result.checkoutUrl) {
441
+ window.location.assign(result.checkoutUrl);
442
+ return;
443
+ }
444
+ await queryClient.invalidateQueries({ queryKey: ["dev-portal-me", apiBaseUrl] });
445
+ }
446
+ });
447
+ }
448
+
389
449
  //#endregion
390
450
  //#region src/utils/demo-session.ts
391
451
  const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
@@ -533,8 +593,24 @@ const AccountPlansPage = () => {
533
593
  const isError = isPreview ? false : livePlans.isError;
534
594
  const { data: me } = useMySubscriptions();
535
595
  const cancelMutation = useCancelSubscription();
596
+ const changeMutation = useChangePlan();
536
597
  const [cancelingSku, setCancelingSku] = useState(null);
598
+ const [switchingSku, setSwitchingSku] = useState(null);
537
599
  const subscriptionBySku = new Map((me?.subscriptions ?? []).map((sub) => [sub.planSku, sub]));
600
+ const activePlanSkus = (me?.subscriptions ?? []).filter((sub) => isActiveSubscription(sub.status)).map((sub) => sub.planSku);
601
+ const currentPlanSku = activePlanSkus.length === 1 ? activePlanSkus[0] : null;
602
+ const handleSwitch = async (toPlanSku) => {
603
+ if (!currentPlanSku) return;
604
+ setSwitchingSku(toPlanSku);
605
+ try {
606
+ await changeMutation.mutateAsync({
607
+ fromPlanSku: currentPlanSku,
608
+ toPlanSku
609
+ });
610
+ } finally {
611
+ setSwitchingSku(null);
612
+ }
613
+ };
538
614
  const handleUnsubscribe = async (planSku) => {
539
615
  setCancelingSku(planSku);
540
616
  try {
@@ -571,6 +647,10 @@ const AccountPlansPage = () => {
571
647
  className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
572
648
  children: cancelMutation.error instanceof Error ? cancelMutation.error.message : "Failed to cancel subscription."
573
649
  }) : null,
650
+ changeMutation.isError ? /* @__PURE__ */ jsx("div", {
651
+ className: "mb-6 rounded-xl border border-destructive/30 bg-destructive/10 p-4 text-sm text-destructive",
652
+ children: changeMutation.error instanceof Error ? changeMutation.error.message : "Failed to change plan."
653
+ }) : null,
574
654
  isLoading ? /* @__PURE__ */ jsx("p", {
575
655
  className: "text-muted-foreground",
576
656
  children: "Loading plans…"
@@ -590,6 +670,8 @@ const AccountPlansPage = () => {
590
670
  subscriptionStatus: subscription?.status,
591
671
  isSubscribed,
592
672
  onUnsubscribe: isPreview || isDemoSession ? void 0 : handleUnsubscribe,
673
+ onSwitch: currentPlanSku && !isPreview && !isDemoSession ? handleSwitch : void 0,
674
+ isSwitching: switchingSku === plan.sku,
593
675
  isCanceling: cancelingSku === plan.sku,
594
676
  cancelDisabled: isPreview || isDemoSession
595
677
  }, plan.sku);
@@ -2570,6 +2652,14 @@ const devPortalBillingPlugin = createPlugin((options = {}) => ({
2570
2652
  handle: { layout: "default" },
2571
2653
  element: /* @__PURE__ */ jsx(ContactSalesPage, {})
2572
2654
  },
2655
+ {
2656
+ path: ACCOUNT_PLANS_PATH,
2657
+ handle: { layout: "default" },
2658
+ element: /* @__PURE__ */ jsx(AccountLayout, {
2659
+ includeBillingNav: true,
2660
+ children: /* @__PURE__ */ jsx(AccountPlansPage, {})
2661
+ })
2662
+ },
2573
2663
  {
2574
2664
  path: SUBSCRIPTIONS_PATH,
2575
2665
  handle: { layout: "default" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo-plugin-dev-portal-billing",
3
- "version": "0.2.24",
3
+ "version": "0.2.29",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zuplo/zudoku",
@@ -46,8 +46,8 @@
46
46
  "react": "19.2.4",
47
47
  "react-dom": "19.2.4",
48
48
  "tsdown": "0.20.3",
49
- "@lukoweb/apitogo": "0.2.24",
50
- "@lukoweb/apitogo-module-user-panel": "0.2.24"
49
+ "@lukoweb/apitogo": "0.2.29",
50
+ "@lukoweb/apitogo-module-user-panel": "0.2.29"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": ">=19.2.0",