@carlonicora/nextjs-jsonapi 1.37.0 → 1.38.1

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 (63) hide show
  1. package/dist/{BlockNoteEditor-GTWR6CPI.mjs → BlockNoteEditor-5CEV5TZT.mjs} +3 -3
  2. package/dist/{BlockNoteEditor-74FHJO7E.js → BlockNoteEditor-VB72JQEO.js} +13 -13
  3. package/dist/{BlockNoteEditor-74FHJO7E.js.map → BlockNoteEditor-VB72JQEO.js.map} +1 -1
  4. package/dist/billing/index.d.mts +12 -2
  5. package/dist/billing/index.d.ts +12 -2
  6. package/dist/billing/index.js +680 -452
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +588 -360
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-53IPQJVH.js → chunk-3EZX4G2E.js} +147 -23
  11. package/dist/chunk-3EZX4G2E.js.map +1 -0
  12. package/dist/{chunk-YVEK3SUS.js → chunk-BYMBRMKS.js} +454 -446
  13. package/dist/chunk-BYMBRMKS.js.map +1 -0
  14. package/dist/{chunk-P7R2DPD6.mjs → chunk-TQ5GRRTM.mjs} +125 -1
  15. package/dist/chunk-TQ5GRRTM.mjs.map +1 -0
  16. package/dist/{chunk-ZUUH4CQC.mjs → chunk-VMK2N3TQ.mjs} +13 -5
  17. package/dist/{chunk-ZUUH4CQC.mjs.map → chunk-VMK2N3TQ.mjs.map} +1 -1
  18. package/dist/client/index.js +3 -3
  19. package/dist/client/index.mjs +2 -2
  20. package/dist/components/index.js +3 -3
  21. package/dist/components/index.mjs +2 -2
  22. package/dist/contexts/index.js +3 -3
  23. package/dist/contexts/index.mjs +2 -2
  24. package/dist/core/index.d.mts +47 -3
  25. package/dist/core/index.d.ts +47 -3
  26. package/dist/core/index.js +8 -2
  27. package/dist/core/index.js.map +1 -1
  28. package/dist/core/index.mjs +7 -1
  29. package/dist/index.d.mts +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +8 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/index.mjs +7 -1
  34. package/dist/server/index.js +3 -3
  35. package/dist/server/index.mjs +1 -1
  36. package/dist/{stripe-subscription.interface-DK7BJaNd.d.ts → stripe-promotion-code.interface-BcJty0rv.d.ts} +18 -1
  37. package/dist/{stripe-subscription.interface-C8uhCYIZ.d.mts → stripe-promotion-code.interface-Dnm2DJKQ.d.mts} +18 -1
  38. package/package.json +1 -1
  39. package/src/billing/index.ts +1 -0
  40. package/src/core/index.ts +1 -0
  41. package/src/core/registry/ModuleRegistry.ts +1 -0
  42. package/src/features/auth/components/forms/Login.tsx +14 -2
  43. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +5 -16
  44. package/src/features/billing/stripe-invoice/data/stripe-invoice.interface.ts +1 -0
  45. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +108 -0
  46. package/src/features/billing/stripe-promotion-code/components/index.ts +1 -0
  47. package/src/features/billing/stripe-promotion-code/data/index.ts +3 -0
  48. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +14 -0
  49. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +64 -0
  50. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.ts +66 -0
  51. package/src/features/billing/stripe-promotion-code/index.ts +2 -0
  52. package/src/features/billing/stripe-promotion-code/stripe-promotion-code.module.ts +9 -0
  53. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +20 -3
  54. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +6 -0
  55. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +131 -9
  56. package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +2 -0
  57. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +8 -0
  58. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +93 -7
  59. package/src/features/index.ts +1 -0
  60. package/dist/chunk-53IPQJVH.js.map +0 -1
  61. package/dist/chunk-P7R2DPD6.mjs.map +0 -1
  62. package/dist/chunk-YVEK3SUS.js.map +0 -1
  63. /package/dist/{BlockNoteEditor-GTWR6CPI.mjs.map → BlockNoteEditor-5CEV5TZT.mjs.map} +0 -0
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from "react";
4
- import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui";
4
+ import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui";
5
5
  import { formatCurrency, formatDate } from "../../../components/utils";
6
6
  import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
7
- import { StripeSubscriptionInterface } from "../../data";
7
+ import { StripeSubscriptionInterface, SubscriptionStatus } from "../../data";
8
8
  import { SubscriptionDetails } from "../details/SubscriptionDetails";
9
9
  import { SubscriptionStatusBadge } from "../widgets/SubscriptionStatusBadge";
10
10
 
@@ -61,12 +61,13 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChan
61
61
  <TableHead>Plan</TableHead>
62
62
  <TableHead>Period</TableHead>
63
63
  <TableHead className="text-right">Amount</TableHead>
64
+ <TableHead className="text-right">Actions</TableHead>
64
65
  </TableRow>
65
66
  </TableHeader>
66
67
  <TableBody>
67
68
  {subscriptions.map((subscription) => {
68
69
  const price = subscription.price;
69
- const amount = price?.unitAmount ? formatCurrency(price.unitAmount, price.currency) : "N/A";
70
+ const amount = price?.unitAmount ? formatCurrency(price.unitAmount, price.currency) : "0";
70
71
  const period = `${formatDate(subscription.currentPeriodStart)} - ${formatDate(subscription.currentPeriodEnd)}`;
71
72
 
72
73
  return (
@@ -84,6 +85,22 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChan
84
85
  <TableCell className="font-medium">{formatPlanName(price)}</TableCell>
85
86
  <TableCell className="text-muted-foreground text-sm">{period}</TableCell>
86
87
  <TableCell className="text-right font-medium">{amount}</TableCell>
88
+ <TableCell className="text-right">
89
+ {(subscription.status === SubscriptionStatus.ACTIVE ||
90
+ subscription.status === SubscriptionStatus.TRIALING) &&
91
+ onChangePlan && (
92
+ <Button
93
+ size="sm"
94
+ variant="outline"
95
+ onClick={(e) => {
96
+ e.stopPropagation();
97
+ onChangePlan(subscription);
98
+ }}
99
+ >
100
+ Upgrade
101
+ </Button>
102
+ )}
103
+ </TableCell>
87
104
  </TableRow>
88
105
  );
89
106
  })}
@@ -103,6 +103,12 @@ export function SubscriptionWizard({
103
103
  onBack={() => actions.goToStep("plan-selection")}
104
104
  onAddPaymentMethod={() => actions.goToStep("payment-method")}
105
105
  onConfirm={actions.confirmSubscription}
106
+ promotionCode={state.promotionCode}
107
+ isValidatingPromoCode={state.isValidatingPromoCode}
108
+ promoCodeError={state.promoCodeError}
109
+ onApplyPromoCode={actions.validatePromoCode}
110
+ onRemovePromoCode={actions.clearPromoCode}
111
+ isTrialUpgrade={state.isTrialSubscription}
106
112
  />
107
113
  )}
108
114
 
@@ -5,6 +5,8 @@ import { Alert, AlertDescription, Button } from "../../../../../shadcnui";
5
5
  import { formatCurrency } from "../../../components/utils/currency";
6
6
  import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
7
7
  import { ProrationPreviewInterface } from "../../../stripe-invoice/data/stripe-invoice.interface";
8
+ import { PromotionCodeValidationResult } from "../../../stripe-promotion-code";
9
+ import { PromoCodeInput } from "../../../stripe-promotion-code/components/PromoCodeInput";
8
10
  import { StripeSubscriptionInterface } from "../../data";
9
11
 
10
12
  type WizardStepReviewProps = {
@@ -17,6 +19,14 @@ type WizardStepReviewProps = {
17
19
  onBack: () => void;
18
20
  onAddPaymentMethod: () => void;
19
21
  onConfirm: () => void;
22
+ // Promotion code props
23
+ promotionCode: PromotionCodeValidationResult | null;
24
+ isValidatingPromoCode: boolean;
25
+ promoCodeError: string | null;
26
+ onApplyPromoCode: (code: string) => void;
27
+ onRemovePromoCode: () => void;
28
+ // Trial upgrade flag
29
+ isTrialUpgrade: boolean;
20
30
  };
21
31
 
22
32
  export function WizardStepReview({
@@ -29,6 +39,12 @@ export function WizardStepReview({
29
39
  onBack,
30
40
  onAddPaymentMethod,
31
41
  onConfirm,
42
+ promotionCode,
43
+ isValidatingPromoCode,
44
+ promoCodeError,
45
+ onApplyPromoCode,
46
+ onRemovePromoCode,
47
+ isTrialUpgrade,
32
48
  }: WizardStepReviewProps) {
33
49
  if (!selectedPrice) {
34
50
  return (
@@ -44,6 +60,62 @@ export function WizardStepReview({
44
60
  return interval === "year" ? "yearly" : "monthly";
45
61
  };
46
62
 
63
+ // Calculate discounted price if promotion code is applied
64
+ const calculateDiscountedPrice = (): number | null => {
65
+ if (!promotionCode?.valid || !selectedPrice.unitAmount) return null;
66
+
67
+ const originalPrice = selectedPrice.unitAmount;
68
+
69
+ if (promotionCode.discountType === "percent_off" && promotionCode.discountValue) {
70
+ return originalPrice * (1 - promotionCode.discountValue / 100);
71
+ }
72
+
73
+ if (promotionCode.discountType === "amount_off" && promotionCode.discountValue) {
74
+ // amount_off is in cents, same as unitAmount
75
+ return Math.max(0, originalPrice - promotionCode.discountValue);
76
+ }
77
+
78
+ return null;
79
+ };
80
+
81
+ const discountedPrice = calculateDiscountedPrice();
82
+
83
+ // Calculate discounted immediate charge for proration preview
84
+ const calculateDiscountedImmediateCharge = (): number | null => {
85
+ if (!promotionCode?.valid || !prorationPreview?.immediateCharge) return null;
86
+
87
+ const originalCharge = prorationPreview.immediateCharge;
88
+
89
+ if (promotionCode.discountType === "percent_off" && promotionCode.discountValue) {
90
+ return originalCharge * (1 - promotionCode.discountValue / 100);
91
+ }
92
+
93
+ if (promotionCode.discountType === "amount_off" && promotionCode.discountValue) {
94
+ return Math.max(0, originalCharge - promotionCode.discountValue);
95
+ }
96
+
97
+ return null;
98
+ };
99
+
100
+ const discountedImmediateCharge = calculateDiscountedImmediateCharge();
101
+
102
+ // Format the discount description
103
+ const getDiscountDescription = (): string | null => {
104
+ if (!promotionCode?.valid) return null;
105
+
106
+ if (promotionCode.discountType === "percent_off" && promotionCode.discountValue) {
107
+ return `${promotionCode.discountValue}% off`;
108
+ }
109
+
110
+ if (promotionCode.discountType === "amount_off" && promotionCode.discountValue) {
111
+ return `${formatCurrency(promotionCode.discountValue, promotionCode.currency || selectedPrice.currency)} off`;
112
+ }
113
+
114
+ return null;
115
+ };
116
+
117
+ const discountDescription = getDiscountDescription();
118
+
47
119
  return (
48
120
  <div className="space-y-6">
49
121
  {/* Selected Plan Summary */}
@@ -55,9 +127,25 @@ export function WizardStepReview({
55
127
  {selectedPrice.nickname && <p className="text-sm text-muted-foreground">{selectedPrice.nickname}</p>}
56
128
  </div>
57
129
  <div className="text-right">
58
- <p className="font-semibold text-lg">
59
- {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency)}
60
- </p>
130
+ {discountedPrice !== null ? (
131
+ <>
132
+ <p className="text-sm text-muted-foreground line-through">
133
+ {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency)}
134
+ </p>
135
+ <p className="font-semibold text-lg text-green-600">
136
+ {formatCurrency(discountedPrice, selectedPrice.currency)}
137
+ </p>
138
+ {discountDescription && (
139
+ <span className="inline-block text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
140
+ {discountDescription}
141
+ </span>
142
+ )}
143
+ </>
144
+ ) : (
145
+ <p className="font-semibold text-lg">
146
+ {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency)}
147
+ </p>
148
+ )}
61
149
  <p className="text-sm text-muted-foreground">{formatInterval(selectedPrice)}</p>
62
150
  </div>
63
151
  </div>
@@ -65,18 +153,52 @@ export function WizardStepReview({
65
153
 
66
154
  {/* Proration Preview (for plan changes) */}
67
155
  {isChangingPlan && prorationPreview && (
68
- <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-2">
69
- <h4 className="font-medium text-blue-800">Proration Summary</h4>
70
- <p className="text-sm text-blue-700">Your next charge will be adjusted to account for the plan change.</p>
156
+ <div
157
+ className={`${isTrialUpgrade ? "bg-amber-50 border-amber-200" : "bg-blue-50 border-blue-200"} border rounded-lg p-4 space-y-2`}
158
+ >
159
+ <h4 className={`font-medium ${isTrialUpgrade ? "text-amber-800" : "text-blue-800"}`}>
160
+ {isTrialUpgrade ? "Trial Upgrade" : "Proration Summary"}
161
+ </h4>
162
+ <p className={`text-sm ${isTrialUpgrade ? "text-amber-700" : "text-blue-700"}`}>
163
+ {isTrialUpgrade
164
+ ? "Your trial will end immediately and you will be charged the full price."
165
+ : "Your next charge will be adjusted to account for the plan change."}
166
+ </p>
71
167
  <div className="flex justify-between text-sm">
72
- <span className="text-blue-600">Amount due now:</span>
73
- <span className="font-medium text-blue-800">
74
- {formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency)}
168
+ <span className={`${isTrialUpgrade ? "text-amber-600" : "text-blue-600"}`}>
169
+ {isTrialUpgrade ? "Amount to charge now:" : "Amount due now:"}
170
+ </span>
171
+ <span className={`font-medium ${isTrialUpgrade ? "text-amber-800" : "text-blue-800"}`}>
172
+ {discountedImmediateCharge !== null ? (
173
+ <>
174
+ <span className="line-through text-muted-foreground mr-2">
175
+ {formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency)}
176
+ </span>
177
+ <span className="text-green-600">
178
+ {formatCurrency(discountedImmediateCharge, prorationPreview.currency)}
179
+ </span>
180
+ </>
181
+ ) : (
182
+ formatCurrency(prorationPreview.immediateCharge, prorationPreview.currency)
183
+ )}
75
184
  </span>
76
185
  </div>
77
186
  </div>
78
187
  )}
79
188
 
189
+ {/* Promotion Code */}
190
+ <div className="border rounded-lg p-4 space-y-3">
191
+ <h4 className="font-medium">Promotion Code</h4>
192
+ <PromoCodeInput
193
+ appliedCode={promotionCode}
194
+ isValidating={isValidatingPromoCode}
195
+ error={promoCodeError}
196
+ onApply={onApplyPromoCode}
197
+ onRemove={onRemovePromoCode}
198
+ disabled={isProcessing}
199
+ />
200
+ </div>
201
+
80
202
  {/* Payment Method Status */}
81
203
  <div className="border rounded-lg p-4">
82
204
  <div className="flex justify-between items-center">
@@ -48,6 +48,8 @@ export type StripeSubscriptionInput = {
48
48
  trialPeriodDays?: number;
49
49
  paymentMethodId?: string;
50
50
  metadata?: Record<string, any>;
51
+ // For CREATE and CHANGE-PLAN - optional promotion code
52
+ promotionCode?: string;
51
53
  };
52
54
 
53
55
  // ============================================================================
@@ -83,6 +83,9 @@ export class StripeSubscription extends AbstractApiData implements StripeSubscri
83
83
  }
84
84
 
85
85
  createJsonApi(data: StripeSubscriptionInput): any {
86
+ console.log("[StripeSubscription.createJsonApi] Input data:", JSON.stringify(data, null, 2));
87
+ console.log("[StripeSubscription.createJsonApi] promotionCode in input:", data.promotionCode);
88
+
86
89
  const response: any = {
87
90
  data: {
88
91
  type: Modules.StripeSubscription.name,
@@ -130,6 +133,11 @@ export class StripeSubscription extends AbstractApiData implements StripeSubscri
130
133
  response.data.attributes.metadata = data.metadata;
131
134
  }
132
135
 
136
+ if (data.promotionCode) {
137
+ response.data.attributes.promotionCode = data.promotionCode;
138
+ }
139
+
140
+ console.log("[StripeSubscription.createJsonApi] Final response:", JSON.stringify(response, null, 2));
133
141
  return response;
134
142
  }
135
143
  }
@@ -5,8 +5,9 @@ import { v4 } from "uuid";
5
5
  import { StripeCustomerService } from "../../stripe-customer/data/stripe-customer.service";
6
6
  import { ProrationPreviewInterface } from "../../stripe-invoice/data/stripe-invoice.interface";
7
7
  import { StripePriceInterface } from "../../stripe-price/data/stripe-price.interface";
8
+ import { PromotionCodeValidationResult, StripePromotionCodeService } from "../../stripe-promotion-code";
8
9
  import { BillingInterval } from "../components/widgets/IntervalToggle";
9
- import { StripeSubscriptionInterface, StripeSubscriptionService } from "../data";
10
+ import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../data";
10
11
 
11
12
  export type WizardStep = "plan-selection" | "review" | "payment-method";
12
13
 
@@ -18,6 +19,12 @@ export type WizardState = {
18
19
  isProcessing: boolean;
19
20
  error: string | null;
20
21
  prorationPreview: ProrationPreviewInterface | null;
22
+ // Promotion code state
23
+ promotionCode: PromotionCodeValidationResult | null;
24
+ isValidatingPromoCode: boolean;
25
+ promoCodeError: string | null;
26
+ // Trial state
27
+ isTrialSubscription: boolean;
21
28
  };
22
29
 
23
30
  type WizardAction =
@@ -28,6 +35,10 @@ type WizardAction =
28
35
  | { type: "SET_PROCESSING"; isProcessing: boolean }
29
36
  | { type: "SET_ERROR"; error: string | null }
30
37
  | { type: "SET_PRORATION_PREVIEW"; preview: ProrationPreviewInterface | null }
38
+ | { type: "SET_PROMOTION_CODE"; code: PromotionCodeValidationResult | null }
39
+ | { type: "SET_VALIDATING_PROMO_CODE"; isValidating: boolean }
40
+ | { type: "SET_PROMO_CODE_ERROR"; error: string | null }
41
+ | { type: "SET_IS_TRIAL_SUBSCRIPTION"; isTrial: boolean }
31
42
  | { type: "RESET" };
32
43
 
33
44
  const initialState: WizardState = {
@@ -38,6 +49,10 @@ const initialState: WizardState = {
38
49
  isProcessing: false,
39
50
  error: null,
40
51
  prorationPreview: null,
52
+ promotionCode: null,
53
+ isValidatingPromoCode: false,
54
+ promoCodeError: null,
55
+ isTrialSubscription: false,
41
56
  };
42
57
 
43
58
  function wizardReducer(state: WizardState, action: WizardAction): WizardState {
@@ -56,6 +71,14 @@ function wizardReducer(state: WizardState, action: WizardAction): WizardState {
56
71
  return { ...state, error: action.error };
57
72
  case "SET_PRORATION_PREVIEW":
58
73
  return { ...state, prorationPreview: action.preview };
74
+ case "SET_PROMOTION_CODE":
75
+ return { ...state, promotionCode: action.code, promoCodeError: null };
76
+ case "SET_VALIDATING_PROMO_CODE":
77
+ return { ...state, isValidatingPromoCode: action.isValidating };
78
+ case "SET_PROMO_CODE_ERROR":
79
+ return { ...state, promoCodeError: action.error };
80
+ case "SET_IS_TRIAL_SUBSCRIPTION":
81
+ return { ...state, isTrialSubscription: action.isTrial };
59
82
  case "RESET":
60
83
  return initialState;
61
84
  default:
@@ -112,6 +135,23 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
112
135
  // Check payment method first
113
136
  await checkPaymentMethod();
114
137
 
138
+ // Check if current subscription is trial
139
+ const isTrialUpgrade = subscription?.status === SubscriptionStatus.TRIALING;
140
+ dispatch({ type: "SET_IS_TRIAL_SUBSCRIPTION", isTrial: isTrialUpgrade });
141
+
142
+ // For trial upgrades, require payment method before review
143
+ if (isTrialUpgrade && !state.hasPaymentMethod) {
144
+ const methods = await StripeCustomerService.listPaymentMethods();
145
+ if (methods.length === 0) {
146
+ dispatch({ type: "SET_STEP", step: "payment-method" });
147
+ dispatch({
148
+ type: "SET_ERROR",
149
+ error: "A payment method is required to upgrade from your trial.",
150
+ });
151
+ return;
152
+ }
153
+ }
154
+
115
155
  // If editing subscription, get proration preview
116
156
  if (subscription && state.selectedPrice.id !== subscription.price?.id) {
117
157
  const preview = await StripeSubscriptionService.getProrationPreview({
@@ -128,27 +168,37 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
128
168
  } finally {
129
169
  dispatch({ type: "SET_PROCESSING", isProcessing: false });
130
170
  }
131
- }, [state.selectedPrice, subscription, checkPaymentMethod]);
171
+ }, [state.selectedPrice, state.hasPaymentMethod, subscription, checkPaymentMethod]);
132
172
 
133
173
  const confirmSubscription = useCallback(async () => {
134
174
  if (!state.selectedPrice) return;
135
175
 
176
+ console.log("[useSubscriptionWizard] confirmSubscription called");
177
+ console.log("[useSubscriptionWizard] state.promotionCode:", JSON.stringify(state.promotionCode, null, 2));
178
+ console.log("[useSubscriptionWizard] promotionCodeId to send:", state.promotionCode?.promotionCodeId);
179
+
136
180
  dispatch({ type: "SET_PROCESSING", isProcessing: true });
137
181
  dispatch({ type: "SET_ERROR", error: null });
138
182
 
139
183
  try {
140
184
  if (subscription) {
141
185
  // Change existing subscription
142
- await StripeSubscriptionService.changePlan({
186
+ const changePlanParams = {
143
187
  id: subscription.id,
144
188
  newPriceId: state.selectedPrice.id,
145
- });
189
+ promotionCode: state.promotionCode?.promotionCodeId,
190
+ };
191
+ console.log("[useSubscriptionWizard] changePlan params:", JSON.stringify(changePlanParams, null, 2));
192
+ await StripeSubscriptionService.changePlan(changePlanParams);
146
193
  } else {
147
194
  // Create new subscription
148
- await StripeSubscriptionService.createSubscription({
195
+ const createParams = {
149
196
  id: v4(),
150
197
  priceId: state.selectedPrice.id,
151
- });
198
+ promotionCode: state.promotionCode?.promotionCodeId,
199
+ };
200
+ console.log("[useSubscriptionWizard] createSubscription params:", JSON.stringify(createParams, null, 2));
201
+ await StripeSubscriptionService.createSubscription(createParams);
152
202
  }
153
203
 
154
204
  onSuccessRef.current();
@@ -176,7 +226,7 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
176
226
  } finally {
177
227
  dispatch({ type: "SET_PROCESSING", isProcessing: false });
178
228
  }
179
- }, [state.selectedPrice, subscription]);
229
+ }, [state.selectedPrice, state.promotionCode, subscription]);
180
230
 
181
231
  const handlePaymentMethodSuccess = useCallback(async () => {
182
232
  dispatch({ type: "SET_HAS_PAYMENT_METHOD", hasMethod: true });
@@ -188,6 +238,38 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
188
238
  dispatch({ type: "RESET" });
189
239
  }, []);
190
240
 
241
+ const validatePromoCode = useCallback(
242
+ async (code: string) => {
243
+ dispatch({ type: "SET_VALIDATING_PROMO_CODE", isValidating: true });
244
+ dispatch({ type: "SET_PROMO_CODE_ERROR", error: null });
245
+
246
+ try {
247
+ const result = await StripePromotionCodeService.validatePromotionCode({
248
+ code,
249
+ stripePriceId: state.selectedPrice?.id,
250
+ });
251
+
252
+ if (result.valid) {
253
+ dispatch({ type: "SET_PROMOTION_CODE", code: result });
254
+ } else {
255
+ dispatch({ type: "SET_PROMO_CODE_ERROR", error: result.errorMessage || "Invalid promotion code" });
256
+ }
257
+ } catch (error: any) {
258
+ console.error("[useSubscriptionWizard] Promo code validation error:", error);
259
+ dispatch({ type: "SET_PROMO_CODE_ERROR", error: error?.message || "Failed to validate promotion code" });
260
+ } finally {
261
+ dispatch({ type: "SET_VALIDATING_PROMO_CODE", isValidating: false });
262
+ }
263
+ },
264
+ [state.selectedPrice?.id],
265
+ );
266
+
267
+ const clearPromoCode = useCallback(() => {
268
+ dispatch({ type: "SET_PROMOTION_CODE", code: null });
269
+ dispatch({ type: "SET_PROMO_CODE_ERROR", error: null });
270
+ dispatch({ type: "SET_ERROR", error: null });
271
+ }, []);
272
+
191
273
  const actions = useMemo(
192
274
  () => ({
193
275
  selectPrice,
@@ -198,6 +280,8 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
198
280
  handlePaymentMethodSuccess,
199
281
  checkPaymentMethod,
200
282
  reset,
283
+ validatePromoCode,
284
+ clearPromoCode,
201
285
  }),
202
286
  [
203
287
  selectPrice,
@@ -208,6 +292,8 @@ export function useSubscriptionWizard({ subscription, onSuccess, onClose }: UseS
208
292
  handlePaymentMethodSuccess,
209
293
  checkPaymentMethod,
210
294
  reset,
295
+ validatePromoCode,
296
+ clearPromoCode,
211
297
  ],
212
298
  );
213
299
 
@@ -7,6 +7,7 @@ export * from "./billing/stripe-price";
7
7
  export * from "./billing/stripe-product";
8
8
  export * from "./billing/stripe-subscription";
9
9
  export * from "./billing/stripe-usage";
10
+ export * from "./billing/stripe-promotion-code";
10
11
  export * from "./company";
11
12
  export * from "./content";
12
13
  export * from "./feature";