@carlonicora/nextjs-jsonapi 1.36.1 → 1.38.0

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 (190) hide show
  1. package/dist/{BlockNoteEditor-4MDHRUS2.js → BlockNoteEditor-3S2B36O3.js} +15 -15
  2. package/dist/{BlockNoteEditor-4MDHRUS2.js.map → BlockNoteEditor-3S2B36O3.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-SZWO3MDO.mjs → BlockNoteEditor-WQUJTVJL.mjs} +5 -5
  4. package/dist/BlockNoteEditor-WQUJTVJL.mjs.map +1 -0
  5. package/dist/billing/index.d.mts +15 -5
  6. package/dist/billing/index.d.ts +15 -5
  7. package/dist/billing/index.js +750 -520
  8. package/dist/billing/index.js.map +1 -1
  9. package/dist/billing/index.mjs +665 -435
  10. package/dist/billing/index.mjs.map +1 -1
  11. package/dist/{chunk-53IPQJVH.js → chunk-3EZX4G2E.js} +147 -23
  12. package/dist/chunk-3EZX4G2E.js.map +1 -0
  13. package/dist/{chunk-I7DFEJFF.mjs → chunk-4PHADEKA.mjs} +738 -1418
  14. package/dist/chunk-4PHADEKA.mjs.map +1 -0
  15. package/dist/{chunk-E6PQQTWF.js → chunk-T2JCZYWK.js} +999 -1679
  16. package/dist/chunk-T2JCZYWK.js.map +1 -0
  17. package/dist/{chunk-P7R2DPD6.mjs → chunk-TQ5GRRTM.mjs} +125 -1
  18. package/dist/chunk-TQ5GRRTM.mjs.map +1 -0
  19. package/dist/client/index.js +3 -3
  20. package/dist/client/index.mjs +2 -2
  21. package/dist/components/index.d.mts +23 -8
  22. package/dist/components/index.d.ts +23 -8
  23. package/dist/components/index.js +3 -3
  24. package/dist/components/index.mjs +2 -2
  25. package/dist/contexts/index.d.mts +1 -1
  26. package/dist/contexts/index.d.ts +1 -1
  27. package/dist/contexts/index.js +3 -3
  28. package/dist/contexts/index.mjs +2 -2
  29. package/dist/core/index.d.mts +47 -3
  30. package/dist/core/index.d.ts +47 -3
  31. package/dist/core/index.js +8 -2
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/index.mjs +7 -1
  34. package/dist/index.d.mts +2 -2
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +8 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.mjs +7 -1
  39. package/dist/server/index.js +3 -3
  40. package/dist/server/index.mjs +1 -1
  41. package/dist/{stripe-subscription.interface-DK7BJaNd.d.ts → stripe-promotion-code.interface-BcJty0rv.d.ts} +18 -1
  42. package/dist/{stripe-subscription.interface-C8uhCYIZ.d.mts → stripe-promotion-code.interface-Dnm2DJKQ.d.mts} +18 -1
  43. package/dist/testing/index.js.map +1 -1
  44. package/dist/testing/index.mjs.map +1 -1
  45. package/package.json +2 -2
  46. package/src/billing/index.ts +1 -0
  47. package/src/client/context/JsonApiProvider.tsx +1 -5
  48. package/src/client/hooks/__tests__/useJsonApiGet.test.tsx +9 -9
  49. package/src/client/hooks/__tests__/useJsonApiMutation.test.tsx +11 -11
  50. package/src/client/hooks/__tests__/useRehydration.test.ts +13 -34
  51. package/src/components/editors/BlockNoteEditor.tsx +2 -2
  52. package/src/components/forms/CommonEditorTrigger.tsx +1 -1
  53. package/src/components/forms/FormCheckbox.tsx +2 -12
  54. package/src/components/forms/FormDate.tsx +1 -6
  55. package/src/components/forms/FormInput.tsx +1 -1
  56. package/src/components/forms/FormPassword.tsx +1 -7
  57. package/src/components/forms/FormSelect.tsx +2 -8
  58. package/src/components/forms/FormSlider.tsx +1 -5
  59. package/src/components/forms/FormSwitch.tsx +1 -5
  60. package/src/components/forms/GdprConsentCheckbox.tsx +2 -8
  61. package/src/components/forms/PasswordInput.tsx +28 -26
  62. package/src/components/forms/__tests__/FormCheckbox.test.tsx +16 -18
  63. package/src/components/forms/__tests__/FormDate.test.tsx +14 -30
  64. package/src/components/forms/__tests__/FormInput.test.tsx +21 -37
  65. package/src/components/forms/__tests__/FormSelect.test.tsx +15 -21
  66. package/src/components/tables/ContentListTable.tsx +1 -1
  67. package/src/components/tables/__tests__/ContentListTable.test.tsx +17 -89
  68. package/src/components/tables/cells/cell.component.tsx +1 -1
  69. package/src/contexts/HeaderChildrenContext.tsx +3 -1
  70. package/src/core/endpoint/__tests__/EndpointCreator.test.ts +2 -7
  71. package/src/core/factories/__tests__/JsonApiDataFactory.test.ts +3 -3
  72. package/src/core/factories/__tests__/RehydrationFactory.test.ts +4 -6
  73. package/src/core/index.ts +1 -0
  74. package/src/core/registry/ModuleRegistry.ts +1 -0
  75. package/src/core/registry/__tests__/DataClassRegistry.test.ts +5 -15
  76. package/src/core/registry/__tests__/ModuleRegistrar.test.ts +5 -15
  77. package/src/features/auth/components/GdprConsentSection.tsx +1 -6
  78. package/src/features/auth/components/details/LandingComponent.tsx +6 -1
  79. package/src/features/auth/components/forms/AcceptInvitation.tsx +1 -1
  80. package/src/features/auth/components/forms/ResetPassword.tsx +1 -1
  81. package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +13 -18
  82. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +12 -17
  83. package/src/features/billing/components/modals/BillingDetailModal.tsx +2 -13
  84. package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +8 -1
  85. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +2 -13
  86. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +2 -12
  87. package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +6 -1
  88. package/src/features/billing/stripe-invoice/data/stripe-invoice.interface.ts +1 -0
  89. package/src/features/billing/stripe-price/components/lists/PricesList.tsx +13 -5
  90. package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +5 -5
  91. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +108 -0
  92. package/src/features/billing/stripe-promotion-code/components/index.ts +1 -0
  93. package/src/features/billing/stripe-promotion-code/data/index.ts +3 -0
  94. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +14 -0
  95. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +64 -0
  96. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.ts +66 -0
  97. package/src/features/billing/stripe-promotion-code/index.ts +2 -0
  98. package/src/features/billing/stripe-promotion-code/stripe-promotion-code.module.ts +9 -0
  99. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +1 -3
  100. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +4 -1
  101. package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +1 -1
  102. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +24 -4
  103. package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +9 -2
  104. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +3 -1
  105. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +7 -7
  106. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +2 -10
  107. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +3 -13
  108. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +134 -23
  109. package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +2 -0
  110. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +8 -0
  111. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +93 -7
  112. package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +1 -1
  113. package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +1 -1
  114. package/src/features/company/components/details/CompanyDetails.tsx +2 -2
  115. package/src/features/company/components/forms/CompanyConfigurationSecurityForm.tsx +1 -1
  116. package/src/features/index.ts +1 -0
  117. package/src/features/notification/components/containers/NotificationsListContainer.tsx +1 -1
  118. package/src/features/notification/components/modals/NotificationModal.tsx +6 -2
  119. package/src/features/notification/contexts/NotificationContext.tsx +1 -3
  120. package/src/features/oauth/components/OAuthClientCard.tsx +15 -17
  121. package/src/features/oauth/components/OAuthClientDetail.tsx +7 -19
  122. package/src/features/oauth/components/OAuthClientForm.tsx +4 -13
  123. package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +4 -20
  124. package/src/features/oauth/components/OAuthRedirectUriInput.tsx +5 -12
  125. package/src/features/oauth/components/OAuthScopeSelector.tsx +17 -23
  126. package/src/features/oauth/components/consent/OAuthConsentActions.tsx +3 -16
  127. package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +3 -12
  128. package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +5 -20
  129. package/src/features/oauth/components/consent/OAuthScopeList.tsx +3 -18
  130. package/src/features/onboarding/contexts/OnboardingContext.tsx +3 -3
  131. package/src/features/role/components/forms/FormRoles.tsx +1 -7
  132. package/src/features/user/components/containers/UserContainer.tsx +1 -1
  133. package/src/features/user/components/details/UserDetails.tsx +1 -1
  134. package/src/features/user/components/forms/UserDeleter.tsx +1 -1
  135. package/src/features/user/components/forms/UserEditor.tsx +1 -1
  136. package/src/features/user/components/forms/UserMultiSelect.tsx +7 -7
  137. package/src/features/user/components/lists/UserListInAdd.tsx +2 -2
  138. package/src/features/user/components/lists/UsersList.tsx +7 -1
  139. package/src/features/user/contexts/CurrentUserContext.tsx +36 -33
  140. package/src/hooks/__tests__/useDataListRetriever.test.ts +15 -21
  141. package/src/hooks/__tests__/useDebounce.test.ts +2 -7
  142. package/src/hooks/useCustomD3Graph.tsx +2 -2
  143. package/src/shadcnui/custom/multi-select.tsx +28 -2
  144. package/src/shadcnui/ui/accordion.tsx +21 -23
  145. package/src/shadcnui/ui/alert-dialog.tsx +45 -62
  146. package/src/shadcnui/ui/alert.tsx +25 -41
  147. package/src/shadcnui/ui/avatar.tsx +23 -36
  148. package/src/shadcnui/ui/badge.tsx +13 -11
  149. package/src/shadcnui/ui/breadcrumb.tsx +21 -55
  150. package/src/shadcnui/ui/button.tsx +17 -18
  151. package/src/shadcnui/ui/calendar.tsx +44 -93
  152. package/src/shadcnui/ui/carousel.tsx +72 -100
  153. package/src/shadcnui/ui/chart.tsx +102 -161
  154. package/src/shadcnui/ui/checkbox.tsx +8 -9
  155. package/src/shadcnui/ui/combobox.tsx +52 -83
  156. package/src/shadcnui/ui/command.tsx +43 -77
  157. package/src/shadcnui/ui/context-menu.tsx +47 -86
  158. package/src/shadcnui/ui/dialog.tsx +34 -60
  159. package/src/shadcnui/ui/drawer.tsx +32 -53
  160. package/src/shadcnui/ui/dropdown-menu.tsx +48 -65
  161. package/src/shadcnui/ui/field.tsx +39 -48
  162. package/src/shadcnui/ui/hover-card.tsx +9 -14
  163. package/src/shadcnui/ui/input-group.tsx +44 -55
  164. package/src/shadcnui/ui/input-otp.tsx +22 -26
  165. package/src/shadcnui/ui/input.tsx +6 -6
  166. package/src/shadcnui/ui/label.tsx +6 -6
  167. package/src/shadcnui/ui/navigation-menu.tsx +36 -60
  168. package/src/shadcnui/ui/popover.tsx +15 -38
  169. package/src/shadcnui/ui/progress.tsx +12 -29
  170. package/src/shadcnui/ui/radio-group.tsx +9 -15
  171. package/src/shadcnui/ui/resizable.tsx +14 -24
  172. package/src/shadcnui/ui/scroll-area.tsx +12 -27
  173. package/src/shadcnui/ui/select.tsx +41 -65
  174. package/src/shadcnui/ui/separator.tsx +7 -11
  175. package/src/shadcnui/ui/sheet.tsx +30 -55
  176. package/src/shadcnui/ui/sidebar.tsx +141 -189
  177. package/src/shadcnui/ui/skeleton.tsx +3 -9
  178. package/src/shadcnui/ui/slider.tsx +11 -23
  179. package/src/shadcnui/ui/switch.tsx +8 -8
  180. package/src/shadcnui/ui/tabs.tsx +14 -21
  181. package/src/shadcnui/ui/textarea.tsx +5 -5
  182. package/src/shadcnui/ui/toggle.tsx +8 -14
  183. package/src/shadcnui/ui/tooltip.tsx +11 -23
  184. package/src/testing/providers/MockJsonApiProvider.tsx +1 -5
  185. package/src/testing/utils/renderWithProviders.tsx +6 -10
  186. package/dist/BlockNoteEditor-SZWO3MDO.mjs.map +0 -1
  187. package/dist/chunk-53IPQJVH.js.map +0 -1
  188. package/dist/chunk-E6PQQTWF.js.map +0 -1
  189. package/dist/chunk-I7DFEJFF.mjs.map +0 -1
  190. package/dist/chunk-P7R2DPD6.mjs.map +0 -1
@@ -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,12 +39,16 @@ 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 (
35
- <div className="text-center py-8 text-muted-foreground">
36
- No plan selected. Please go back and select a plan.
37
- </div>
51
+ <div className="text-center py-8 text-muted-foreground">No plan selected. Please go back and select a plan.</div>
38
52
  );
39
53
  }
40
54
 
@@ -46,6 +60,62 @@ export function WizardStepReview({
46
60
  return interval === "year" ? "yearly" : "monthly";
47
61
  };
48
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
+
49
119
  return (
50
120
  <div className="space-y-6">
51
121
  {/* Selected Plan Summary */}
@@ -54,14 +124,28 @@ export function WizardStepReview({
54
124
  <div className="flex justify-between items-center">
55
125
  <div>
56
126
  <p className="font-medium">{selectedPrice.product?.name}</p>
57
- {selectedPrice.nickname && (
58
- <p className="text-sm text-muted-foreground">{selectedPrice.nickname}</p>
59
- )}
127
+ {selectedPrice.nickname && <p className="text-sm text-muted-foreground">{selectedPrice.nickname}</p>}
60
128
  </div>
61
129
  <div className="text-right">
62
- <p className="font-semibold text-lg">
63
- {formatCurrency(selectedPrice.unitAmount || 0, selectedPrice.currency)}
64
- </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
+ )}
65
149
  <p className="text-sm text-muted-foreground">{formatInterval(selectedPrice)}</p>
66
150
  </div>
67
151
  </div>
@@ -69,29 +153,59 @@ export function WizardStepReview({
69
153
 
70
154
  {/* Proration Preview (for plan changes) */}
71
155
  {isChangingPlan && prorationPreview && (
72
- <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-2">
73
- <h4 className="font-medium text-blue-800">Proration Summary</h4>
74
- <p className="text-sm text-blue-700">
75
- Your next charge will be adjusted to account for the plan change.
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."}
76
166
  </p>
77
167
  <div className="flex justify-between text-sm">
78
- <span className="text-blue-600">Amount due now:</span>
79
- <span className="font-medium text-blue-800">
80
- {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
+ )}
81
184
  </span>
82
185
  </div>
83
186
  </div>
84
187
  )}
85
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
+
86
202
  {/* Payment Method Status */}
87
203
  <div className="border rounded-lg p-4">
88
204
  <div className="flex justify-between items-center">
89
205
  <div>
90
206
  <h4 className="font-medium">Payment Method</h4>
91
207
  <p className="text-sm text-muted-foreground">
92
- {hasPaymentMethod
93
- ? "A payment method is on file"
94
- : "No payment method on file"}
208
+ {hasPaymentMethod ? "A payment method is on file" : "No payment method on file"}
95
209
  </p>
96
210
  </div>
97
211
  {!hasPaymentMethod && (
@@ -115,10 +229,7 @@ export function WizardStepReview({
115
229
  <Button variant="outline" onClick={onBack} disabled={isProcessing}>
116
230
  Back
117
231
  </Button>
118
- <Button
119
- onClick={hasPaymentMethod ? onConfirm : onAddPaymentMethod}
120
- disabled={isProcessing}
121
- >
232
+ <Button onClick={hasPaymentMethod ? onConfirm : onAddPaymentMethod} disabled={isProcessing}>
122
233
  {isProcessing
123
234
  ? "Processing..."
124
235
  : hasPaymentMethod
@@ -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
 
@@ -30,7 +30,7 @@ function formatDate(date: Date | undefined): string {
30
30
  month: "short",
31
31
  day: "numeric",
32
32
  }).format(new Date(date));
33
- } catch (error) {
33
+ } catch (_error) {
34
34
  return "Invalid Date";
35
35
  }
36
36
  }
@@ -23,7 +23,7 @@ function formatDateTime(date: Date | string | undefined): string {
23
23
  hour: "numeric",
24
24
  minute: "2-digit",
25
25
  }).format(dateObj);
26
- } catch (error) {
26
+ } catch (_error) {
27
27
  return "Invalid Date";
28
28
  }
29
29
  }
@@ -9,9 +9,9 @@ import { usePageUrlGenerator } from "../../../../hooks";
9
9
  import { useCompanyContext } from "../../contexts/CompanyContext";
10
10
 
11
11
  export function CompanyDetails() {
12
- const t = useTranslations();
12
+ const _t = useTranslations();
13
13
  const { title } = useSharedContext();
14
- const generateUrl = usePageUrlGenerator();
14
+ const _generateUrl = usePageUrlGenerator();
15
15
 
16
16
  const { company } = useCompanyContext();
17
17
  if (!company) return null;
@@ -20,7 +20,7 @@ const providerConfig: Fields = {
20
20
  };
21
21
 
22
22
  export function CompanyConfigurationSecurityForm({ form }: SecurityConfigurationFormProps) {
23
- const t = useTranslations();
23
+ const _t = useTranslations();
24
24
 
25
25
  const renderProviderFields = () => {
26
26
  const config = providerConfig;
@@ -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";
@@ -8,7 +8,7 @@ import { NotificationsList } from "../lists/NotificationsList";
8
8
 
9
9
  function NotificationsListContainerContent() {
10
10
  const t = useTranslations();
11
- const { notifications, isLoading, error } = useNotificationContext();
11
+ const { notifications: _notifications, isLoading: _isLoading, error } = useNotificationContext();
12
12
 
13
13
  if (error) {
14
14
  return (
@@ -25,7 +25,7 @@ interface NotificationModalProps {
25
25
  }
26
26
 
27
27
  function NotificationModalContent({ isOpen, setIsOpen }: NotificationModalProps) {
28
- const instanceId = useRef(Math.random().toString(36).substr(2, 9));
28
+ const _instanceId = useRef(Math.random().toString(36).substr(2, 9));
29
29
  const {
30
30
  notifications,
31
31
  addNotification,
@@ -38,7 +38,11 @@ function NotificationModalContent({ isOpen, setIsOpen }: NotificationModalProps)
38
38
  shouldRefresh,
39
39
  lastLoaded,
40
40
  } = useNotificationContext();
41
- const { socketNotifications, removeSocketNotification, clearSocketNotifications } = useSocketContext();
41
+ const {
42
+ socketNotifications,
43
+ removeSocketNotification: _removeSocketNotification,
44
+ clearSocketNotifications,
45
+ } = useSocketContext();
42
46
  const t = useTranslations();
43
47
  const generateUrl = usePageUrlGenerator();
44
48
  const [newNotifications, setNewNotifications] = useState<boolean>(false);
@@ -101,9 +101,7 @@ export const NotificationContextProvider = ({ children }: NotificationContextPro
101
101
 
102
102
  // Skip for admin users
103
103
  if (isRolesConfigured()) {
104
- const isAdmin = currentUser.roles?.some(
105
- (role: RoleInterface) => role.id === getRoleId().Administrator,
106
- );
104
+ const isAdmin = currentUser.roles?.some((role: RoleInterface) => role.id === getRoleId().Administrator);
107
105
  if (isAdmin) {
108
106
  setHasInitiallyLoaded(true);
109
107
  return;
@@ -31,16 +31,10 @@ export interface OAuthClientCardProps {
31
31
  /**
32
32
  * Card component for displaying an OAuth client in a list
33
33
  */
34
- export function OAuthClientCard({
35
- client,
36
- onClick,
37
- onEdit,
38
- onDelete,
39
- }: OAuthClientCardProps) {
34
+ export function OAuthClientCard({ client, onClick, onEdit, onDelete }: OAuthClientCardProps) {
40
35
  // Truncate client ID for display
41
- const truncatedId = client.clientId.length > 12
42
- ? `${client.clientId.slice(0, 8)}...${client.clientId.slice(-4)}`
43
- : client.clientId;
36
+ const truncatedId =
37
+ client.clientId.length > 12 ? `${client.clientId.slice(0, 8)}...${client.clientId.slice(-4)}` : client.clientId;
44
38
 
45
39
  const createdAgo = client.createdAt
46
40
  ? formatDistanceToNow(new Date(client.createdAt), { addSuffix: true })
@@ -58,9 +52,7 @@ export function OAuthClientCard({
58
52
  <CardTitle className="text-lg">{client.name}</CardTitle>
59
53
  </div>
60
54
  <div className="flex items-center gap-2">
61
- <Badge variant={client.isActive ? "default" : "secondary"}>
62
- {client.isActive ? "Active" : "Inactive"}
63
- </Badge>
55
+ <Badge variant={client.isActive ? "default" : "secondary"}>{client.isActive ? "Active" : "Inactive"}</Badge>
64
56
  {(onEdit || onDelete) && (
65
57
  <DropdownMenu>
66
58
  <DropdownMenuTrigger onClick={(e) => e.stopPropagation()}>
@@ -70,14 +62,22 @@ export function OAuthClientCard({
70
62
  </DropdownMenuTrigger>
71
63
  <DropdownMenuContent align="end">
72
64
  {onEdit && (
73
- <DropdownMenuItem onClick={(e) => { e.stopPropagation(); onEdit(); }}>
65
+ <DropdownMenuItem
66
+ onClick={(e) => {
67
+ e.stopPropagation();
68
+ onEdit();
69
+ }}
70
+ >
74
71
  <Pencil className="h-4 w-4 mr-2" />
75
72
  Edit
76
73
  </DropdownMenuItem>
77
74
  )}
78
75
  {onDelete && (
79
76
  <DropdownMenuItem
80
- onClick={(e) => { e.stopPropagation(); onDelete(); }}
77
+ onClick={(e) => {
78
+ e.stopPropagation();
79
+ onDelete();
80
+ }}
81
81
  className="text-destructive"
82
82
  >
83
83
  <Trash2 className="h-4 w-4 mr-2" />
@@ -89,9 +89,7 @@ export function OAuthClientCard({
89
89
  )}
90
90
  </div>
91
91
  </div>
92
- {client.description && (
93
- <CardDescription className="line-clamp-2">{client.description}</CardDescription>
94
- )}
92
+ {client.description && <CardDescription className="line-clamp-2">{client.description}</CardDescription>}
95
93
  </CardHeader>
96
94
  <CardContent>
97
95
  <div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">