@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
@@ -2,13 +2,7 @@
2
2
 
3
3
  import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
4
4
  import { useEffect, useState } from "react";
5
- import {
6
- Alert,
7
- AlertDescription,
8
- Button,
9
- Checkbox,
10
- Label,
11
- } from "../../../../../shadcnui";
5
+ import { Alert, AlertDescription, Button, Checkbox, Label } from "../../../../../shadcnui";
12
6
  import { StripeCustomerService } from "../../data";
13
7
 
14
8
  type PaymentMethodFormProps = {
@@ -137,11 +131,7 @@ export function PaymentMethodForm({ onSuccess, onCancel, isLoading = false }: Pa
137
131
 
138
132
  {/* Set as Default Checkbox */}
139
133
  <div className="flex items-center gap-x-2">
140
- <Checkbox
141
- id="setAsDefault"
142
- checked={setAsDefault}
143
- onCheckedChange={(checked) => setSetAsDefault(!!checked)}
144
- />
134
+ <Checkbox id="setAsDefault" checked={setAsDefault} onCheckedChange={(checked) => setSetAsDefault(!!checked)} />
145
135
  <Label htmlFor="setAsDefault" className="text-sm font-normal">
146
136
  Set as default payment method
147
137
  </Label>
@@ -13,7 +13,12 @@ type InvoiceDetailsProps = {
13
13
  onInvoiceChange: () => void;
14
14
  };
15
15
 
16
- export function InvoiceDetails({ invoice, open, onOpenChange, onInvoiceChange }: InvoiceDetailsProps) {
16
+ export function InvoiceDetails({
17
+ invoice,
18
+ open,
19
+ onOpenChange,
20
+ onInvoiceChange: _onInvoiceChange,
21
+ }: InvoiceDetailsProps) {
17
22
  const handleDownloadPDF = () => {
18
23
  if (invoice.stripePdfUrl) {
19
24
  window.open(invoice.stripePdfUrl, "_blank");
@@ -52,6 +52,7 @@ export interface ProrationPreviewInterface {
52
52
  immediateCharge: number;
53
53
  prorationDate: Date;
54
54
  lineItems: ProrationLineItem[];
55
+ isTrialUpgrade?: boolean;
55
56
  }
56
57
 
57
58
  export interface ProrationLineItem {
@@ -184,13 +184,19 @@ export function PricesList({ productId, onPricesChange }: PricesListProps) {
184
184
 
185
185
  <div className="flex flex-wrap gap-2">
186
186
  {price.active ? (
187
- <span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium">Active</span>
187
+ <span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium">
188
+ Active
189
+ </span>
188
190
  ) : (
189
- <span className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full font-medium">Inactive</span>
191
+ <span className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full font-medium">
192
+ Inactive
193
+ </span>
190
194
  )}
191
195
 
192
196
  {price.recurring?.usageType === "metered" && (
193
- <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium">Metered</span>
197
+ <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium">
198
+ Metered
199
+ </span>
194
200
  )}
195
201
 
196
202
  <span className="bg-gray-100 text-gray-800 text-xs px-2 py-1 rounded-full font-medium uppercase">
@@ -238,7 +244,8 @@ export function PricesList({ productId, onPricesChange }: PricesListProps) {
238
244
  <AlertDialogTitle>Archive Price</AlertDialogTitle>
239
245
  <AlertDialogDescription>
240
246
  Are you sure you want to archive the price for{" "}
241
- {priceToArchive && `${formatCurrency(priceToArchive.unitAmount, priceToArchive.currency)} ${formatInterval(priceToArchive)}`}
247
+ {priceToArchive &&
248
+ `${formatCurrency(priceToArchive.unitAmount, priceToArchive.currency)} ${formatInterval(priceToArchive)}`}
242
249
  ? This will prevent new subscriptions but existing ones will continue.
243
250
  </AlertDialogDescription>
244
251
  </AlertDialogHeader>
@@ -262,7 +269,8 @@ export function PricesList({ productId, onPricesChange }: PricesListProps) {
262
269
  <AlertDialogTitle>Reactivate Price</AlertDialogTitle>
263
270
  <AlertDialogDescription>
264
271
  Are you sure you want to reactivate the price for{" "}
265
- {priceToReactivate && `${formatCurrency(priceToReactivate.unitAmount, priceToReactivate.currency)} ${formatInterval(priceToReactivate)}`}
272
+ {priceToReactivate &&
273
+ `${formatCurrency(priceToReactivate.unitAmount, priceToReactivate.currency)} ${formatInterval(priceToReactivate)}`}
266
274
  ? This will allow new subscriptions again.
267
275
  </AlertDialogDescription>
268
276
  </AlertDialogHeader>
@@ -38,11 +38,11 @@ export function ProductsList({ products, onProductsChange }: ProductsListProps)
38
38
 
39
39
  setArchivingProductId(productToArchive.id);
40
40
  try {
41
- const archivedProduct = await StripeProductService.archiveProduct({ id: productToArchive.id });
41
+ const _archivedProduct = await StripeProductService.archiveProduct({ id: productToArchive.id });
42
42
  setProductToArchive(null); // Close dialog on success
43
43
  onProductsChange();
44
- } catch (error) {
45
- console.error("[ProductsList] Failed to archive product:", error);
44
+ } catch (_error) {
45
+ console.error("[ProductsList] Failed to archive product:", _error);
46
46
  // Keep dialog open on error so user can retry or cancel
47
47
  } finally {
48
48
  setArchivingProductId(null);
@@ -56,10 +56,10 @@ export function ProductsList({ products, onProductsChange }: ProductsListProps)
56
56
 
57
57
  setReactivatingProductId(productToReactivate.id);
58
58
  try {
59
- const reactivatedProduct = await StripeProductService.reactivateProduct({ id: productToReactivate.id });
59
+ const _reactivatedProduct = await StripeProductService.reactivateProduct({ id: productToReactivate.id });
60
60
  setProductToReactivate(null); // Close dialog on success
61
61
  onProductsChange();
62
- } catch (error) {
62
+ } catch (_error) {
63
63
  // Keep dialog open on error so user can retry or cancel
64
64
  } finally {
65
65
  setReactivatingProductId(null);
@@ -0,0 +1,108 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Check, X, Loader2 } from "lucide-react";
5
+ import { Button, Input } from "../../../../shadcnui";
6
+ import { PromotionCodeValidationResult } from "../data/stripe-promotion-code.interface";
7
+
8
+ type PromoCodeInputProps = {
9
+ appliedCode: PromotionCodeValidationResult | null;
10
+ isValidating: boolean;
11
+ error: string | null;
12
+ onApply: (code: string) => void;
13
+ onRemove: () => void;
14
+ disabled?: boolean;
15
+ };
16
+
17
+ export function PromoCodeInput({
18
+ appliedCode,
19
+ isValidating,
20
+ error,
21
+ onApply,
22
+ onRemove,
23
+ disabled = false,
24
+ }: PromoCodeInputProps) {
25
+ const [code, setCode] = useState("");
26
+
27
+ const handleApply = () => {
28
+ if (code.trim()) {
29
+ onApply(code.trim().toUpperCase());
30
+ }
31
+ };
32
+
33
+ const handleKeyDown = (e: React.KeyboardEvent) => {
34
+ if (e.key === "Enter") {
35
+ e.preventDefault();
36
+ handleApply();
37
+ }
38
+ };
39
+
40
+ // Format discount for display
41
+ const formatDiscount = (result: PromotionCodeValidationResult): string => {
42
+ if (result.discountType === "percent_off") {
43
+ return `${result.discountValue}% off`;
44
+ }
45
+ // amount_off is in cents, convert to dollars
46
+ const amount = (result.discountValue || 0) / 100;
47
+ const currency = result.currency?.toUpperCase() || "USD";
48
+ return `${currency} ${amount.toFixed(2)} off`;
49
+ };
50
+
51
+ // Format duration for display
52
+ const formatDuration = (result: PromotionCodeValidationResult): string => {
53
+ switch (result.duration) {
54
+ case "forever":
55
+ return "Applied to all payments";
56
+ case "once":
57
+ return "Applied to first payment only";
58
+ case "repeating":
59
+ return `Applied for ${result.durationInMonths} months`;
60
+ default:
61
+ return "";
62
+ }
63
+ };
64
+
65
+ // Show applied code state
66
+ if (appliedCode?.valid) {
67
+ return (
68
+ <div className="space-y-2">
69
+ <div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg">
70
+ <div className="flex items-center gap-2">
71
+ <Check className="h-4 w-4 text-green-600" />
72
+ <span className="font-medium text-green-800">{appliedCode.code}</span>
73
+ <span className="text-sm text-green-600">{formatDiscount(appliedCode)}</span>
74
+ </div>
75
+ <Button
76
+ variant="ghost"
77
+ size="sm"
78
+ onClick={onRemove}
79
+ disabled={disabled}
80
+ className="text-green-700 hover:text-green-900 hover:bg-green-100"
81
+ >
82
+ <X className="h-4 w-4" />
83
+ </Button>
84
+ </div>
85
+ <p className="text-sm text-green-600">{formatDuration(appliedCode)}</p>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="space-y-2">
92
+ <div className="flex gap-2">
93
+ <Input
94
+ placeholder="Enter promo code"
95
+ value={code}
96
+ onChange={(e) => setCode(e.target.value.toUpperCase())}
97
+ onKeyDown={handleKeyDown}
98
+ disabled={disabled || isValidating}
99
+ className="flex-1"
100
+ />
101
+ <Button variant="outline" onClick={handleApply} disabled={disabled || isValidating || !code.trim()}>
102
+ {isValidating ? <Loader2 className="h-4 w-4 animate-spin" /> : "Apply"}
103
+ </Button>
104
+ </div>
105
+ {error && <p className="text-sm text-red-500">{error}</p>}
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1 @@
1
+ export * from "./PromoCodeInput";
@@ -0,0 +1,3 @@
1
+ export * from "./stripe-promotion-code.interface";
2
+ export * from "./stripe-promotion-code.service";
3
+ export * from "./stripe-promotion-code";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Promotion code validation result from the backend
3
+ */
4
+ export interface PromotionCodeValidationResult {
5
+ valid: boolean;
6
+ promotionCodeId?: string;
7
+ code: string;
8
+ discountType?: "percent_off" | "amount_off";
9
+ discountValue?: number;
10
+ currency?: string;
11
+ duration?: "forever" | "once" | "repeating";
12
+ durationInMonths?: number;
13
+ errorMessage?: string;
14
+ }
@@ -0,0 +1,64 @@
1
+ import { Modules } from "../../../../core";
2
+ import { JsonApiPost } from "../../../../unified/JsonApiRequest";
3
+ import { PromotionCodeValidationResult } from "./stripe-promotion-code.interface";
4
+ import { StripePromotionCode } from "./stripe-promotion-code";
5
+
6
+ const STRIPE_PROMOTION_CODE_ENDPOINT = "stripe-promotion-codes";
7
+
8
+ /**
9
+ * Service for validating Stripe promotion codes
10
+ */
11
+ export class StripePromotionCodeService {
12
+ /**
13
+ * Validate a promotion code against Stripe
14
+ *
15
+ * @param params.code - The promotion code to validate (e.g., "SAVE20")
16
+ * @param params.stripePriceId - Optional price ID to check product restrictions
17
+ * @param params.language - Language code for the request
18
+ * @returns Validation result with discount details if valid
19
+ */
20
+ static async validatePromotionCode(params: {
21
+ code: string;
22
+ stripePriceId?: string;
23
+ language?: string;
24
+ }): Promise<PromotionCodeValidationResult> {
25
+ const response = await JsonApiPost({
26
+ classKey: Modules.StripePromotionCode,
27
+ endpoint: `${STRIPE_PROMOTION_CODE_ENDPOINT}/validate`,
28
+ body: {
29
+ data: {
30
+ type: STRIPE_PROMOTION_CODE_ENDPOINT,
31
+ attributes: {
32
+ code: params.code,
33
+ stripePriceId: params.stripePriceId,
34
+ },
35
+ },
36
+ },
37
+ overridesJsonApiCreation: true,
38
+ language: params.language ?? "en",
39
+ });
40
+
41
+ if (!response.ok) {
42
+ return {
43
+ valid: false,
44
+ code: params.code,
45
+ errorMessage: response.error || "Failed to validate promotion code",
46
+ };
47
+ }
48
+
49
+ // The response is hydrated as a StripePromotionCode model
50
+ const data = response.data as StripePromotionCode;
51
+
52
+ return {
53
+ valid: data.valid,
54
+ promotionCodeId: data.promotionCodeId,
55
+ code: data.code,
56
+ discountType: data.discountType,
57
+ discountValue: data.discountValue,
58
+ currency: data.currency,
59
+ duration: data.duration,
60
+ durationInMonths: data.durationInMonths,
61
+ errorMessage: data.errorMessage,
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,66 @@
1
+ import { AbstractApiData, JsonApiHydratedDataInterface } from "../../../../core";
2
+ import { PromotionCodeValidationResult } from "./stripe-promotion-code.interface";
3
+
4
+ export class StripePromotionCode extends AbstractApiData implements PromotionCodeValidationResult {
5
+ private _valid: boolean = false;
6
+ private _promotionCodeId?: string;
7
+ private _code: string = "";
8
+ private _discountType?: "percent_off" | "amount_off";
9
+ private _discountValue?: number;
10
+ private _currency?: string;
11
+ private _duration?: "forever" | "once" | "repeating";
12
+ private _durationInMonths?: number;
13
+ private _errorMessage?: string;
14
+
15
+ get valid(): boolean {
16
+ return this._valid;
17
+ }
18
+
19
+ get promotionCodeId(): string | undefined {
20
+ return this._promotionCodeId;
21
+ }
22
+
23
+ get code(): string {
24
+ return this._code;
25
+ }
26
+
27
+ get discountType(): "percent_off" | "amount_off" | undefined {
28
+ return this._discountType;
29
+ }
30
+
31
+ get discountValue(): number | undefined {
32
+ return this._discountValue;
33
+ }
34
+
35
+ get currency(): string | undefined {
36
+ return this._currency;
37
+ }
38
+
39
+ get duration(): "forever" | "once" | "repeating" | undefined {
40
+ return this._duration;
41
+ }
42
+
43
+ get durationInMonths(): number | undefined {
44
+ return this._durationInMonths;
45
+ }
46
+
47
+ get errorMessage(): string | undefined {
48
+ return this._errorMessage;
49
+ }
50
+
51
+ rehydrate(data: JsonApiHydratedDataInterface): this {
52
+ super.rehydrate(data);
53
+
54
+ this._valid = data.jsonApi.attributes.valid ?? false;
55
+ this._promotionCodeId = data.jsonApi.attributes.promotionCodeId;
56
+ this._code = data.jsonApi.attributes.code ?? "";
57
+ this._discountType = data.jsonApi.attributes.discountType;
58
+ this._discountValue = data.jsonApi.attributes.discountValue;
59
+ this._currency = data.jsonApi.attributes.currency;
60
+ this._duration = data.jsonApi.attributes.duration;
61
+ this._durationInMonths = data.jsonApi.attributes.durationInMonths;
62
+ this._errorMessage = data.jsonApi.attributes.errorMessage;
63
+
64
+ return this;
65
+ }
66
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./data";
2
+ export * from "./stripe-promotion-code.module";
@@ -0,0 +1,9 @@
1
+ import { ModuleFactory } from "../../../permissions";
2
+ import { StripePromotionCode } from "./data/stripe-promotion-code";
3
+
4
+ export const StripePromotionCodeModule = (factory: ModuleFactory) =>
5
+ factory({
6
+ name: "stripe-promotion-codes",
7
+ model: StripePromotionCode,
8
+ moduleId: "b8e4f6a2-9d0c-5b3f-c7e8-4g2d9f0a5c6e",
9
+ });
@@ -75,9 +75,7 @@ export function SubscriptionsContainer({ onOpenWizard, hasActiveRecurringSubscri
75
75
  <CreditCard className="h-16 w-16 text-muted-foreground" />
76
76
  <div className="text-center">
77
77
  <h3 className="text-xl font-semibold mb-2">No Active Subscriptions</h3>
78
- <p className="text-muted-foreground mb-6">
79
- Choose a subscription plan to get started with our services.
80
- </p>
78
+ <p className="text-muted-foreground mb-6">Choose a subscription plan to get started with our services.</p>
81
79
  <Button onClick={() => onOpenWizard?.()}>Subscribe to a Plan</Button>
82
80
  </div>
83
81
  </div>
@@ -118,7 +118,10 @@ export function SubscriptionDetails({
118
118
  {/* Status */}
119
119
  <div className="flex items-center gap-x-3">
120
120
  <span className="text-sm font-medium text-muted-foreground">Status:</span>
121
- <SubscriptionStatusBadge status={subscription.status} cancelAtPeriodEnd={subscription.cancelAtPeriodEnd} />
121
+ <SubscriptionStatusBadge
122
+ status={subscription.status}
123
+ cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}
124
+ />
122
125
  </div>
123
126
 
124
127
  {/* Plan Info */}
@@ -43,7 +43,7 @@ export function CancelSubscriptionDialog({
43
43
  },
44
44
  });
45
45
 
46
- const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (values) => {
46
+ const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (_values) => {
47
47
  setIsSubmitting(true);
48
48
 
49
49
  try {
@@ -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 (
@@ -76,11 +77,30 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChan
76
77
  className="cursor-pointer hover:bg-muted/50"
77
78
  >
78
79
  <TableCell>
79
- <SubscriptionStatusBadge status={subscription.status} cancelAtPeriodEnd={subscription.cancelAtPeriodEnd} />
80
+ <SubscriptionStatusBadge
81
+ status={subscription.status}
82
+ cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}
83
+ />
80
84
  </TableCell>
81
85
  <TableCell className="font-medium">{formatPlanName(price)}</TableCell>
82
86
  <TableCell className="text-muted-foreground text-sm">{period}</TableCell>
83
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>
84
104
  </TableRow>
85
105
  );
86
106
  })}
@@ -16,7 +16,14 @@ export type PricingCardProps = {
16
16
  onSelect: (price: StripePriceInterface) => void;
17
17
  };
18
18
 
19
- export function PricingCard({ price, isCurrentPlan = false, isSelected = false, isDisabled = false, isLoading = false, onSelect }: PricingCardProps) {
19
+ export function PricingCard({
20
+ price,
21
+ isCurrentPlan = false,
22
+ isSelected = false,
23
+ isDisabled = false,
24
+ isLoading = false,
25
+ onSelect,
26
+ }: PricingCardProps) {
20
27
  const description = price.description || price.nickname || "Standard";
21
28
  const features = price.features || [];
22
29
  const formattedPrice = formatCurrency(price.unitAmount, price.currency);
@@ -50,7 +57,7 @@ export function PricingCard({ price, isCurrentPlan = false, isSelected = false,
50
57
  isSelected && !isCurrentPlan && "ring-2 ring-primary",
51
58
  !isDisabled && !isCurrentPlan && "hover:shadow-md hover:border-primary/50",
52
59
  isDisabled && "opacity-50 pointer-events-none",
53
- isLoading && "pointer-events-none"
60
+ isLoading && "pointer-events-none",
54
61
  )}
55
62
  >
56
63
  {isCurrentPlan && (
@@ -54,7 +54,9 @@ const cancelingConfig: StatusConfig = {
54
54
 
55
55
  export function SubscriptionStatusBadge({ status, cancelAtPeriodEnd }: SubscriptionStatusBadgeProps) {
56
56
  // Show "Canceling" when subscription is set to cancel at period end
57
- const config = cancelAtPeriodEnd ? cancelingConfig : statusConfig[status] || statusConfig[SubscriptionStatus.CANCELED];
57
+ const config = cancelAtPeriodEnd
58
+ ? cancelingConfig
59
+ : statusConfig[status] || statusConfig[SubscriptionStatus.CANCELED];
58
60
 
59
61
  return <span className={`${config.color} text-xs px-2 py-1 rounded-full font-medium`}>{config.label}</span>;
60
62
  }
@@ -1,13 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useCallback, useEffect, useRef } from "react";
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogDescription,
8
- DialogHeader,
9
- DialogTitle,
10
- } from "../../../../../shadcnui";
4
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../../../../shadcnui";
11
5
  import { StripeSubscriptionInterface } from "../../data";
12
6
  import { useSubscriptionWizard } from "../../hooks/useSubscriptionWizard";
13
7
  import { WizardProgressIndicator } from "./WizardProgressIndicator";
@@ -109,6 +103,12 @@ export function SubscriptionWizard({
109
103
  onBack={() => actions.goToStep("plan-selection")}
110
104
  onAddPaymentMethod={() => actions.goToStep("payment-method")}
111
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}
112
112
  />
113
113
  )}
114
114
 
@@ -42,21 +42,13 @@ export function WizardProgressIndicator({ currentStep }: WizardProgressIndicator
42
42
  </div>
43
43
 
44
44
  {/* Step Label */}
45
- <span
46
- className={`text-sm ${
47
- isCurrent ? "font-medium text-foreground" : "text-muted-foreground"
48
- }`}
49
- >
45
+ <span className={`text-sm ${isCurrent ? "font-medium text-foreground" : "text-muted-foreground"}`}>
50
46
  {step.label}
51
47
  </span>
52
48
 
53
49
  {/* Connector */}
54
50
  {index < STEPS.length - 1 && (
55
- <div
56
- className={`h-0.5 w-8 ${
57
- index < currentIndex ? "bg-primary" : "bg-muted"
58
- }`}
59
- />
51
+ <div className={`h-0.5 w-8 ${index < currentIndex ? "bg-primary" : "bg-muted"}`} />
60
52
  )}
61
53
  </div>
62
54
  );
@@ -8,25 +8,15 @@ type WizardStepPaymentMethodProps = {
8
8
  isProcessing: boolean;
9
9
  };
10
10
 
11
- export function WizardStepPaymentMethod({
12
- onBack,
13
- onSuccess,
14
- isProcessing,
15
- }: WizardStepPaymentMethodProps) {
11
+ export function WizardStepPaymentMethod({ onBack, onSuccess, isProcessing }: WizardStepPaymentMethodProps) {
16
12
  return (
17
13
  <div className="space-y-6">
18
14
  <div className="text-center">
19
15
  <h3 className="font-semibold text-lg">Add Payment Method</h3>
20
- <p className="text-sm text-muted-foreground">
21
- Enter your card details to complete your subscription
22
- </p>
16
+ <p className="text-sm text-muted-foreground">Enter your card details to complete your subscription</p>
23
17
  </div>
24
18
 
25
- <PaymentMethodForm
26
- onSuccess={onSuccess}
27
- onCancel={onBack}
28
- isLoading={isProcessing}
29
- />
19
+ <PaymentMethodForm onSuccess={onSuccess} onCancel={onBack} isLoading={isProcessing} />
30
20
  </div>
31
21
  );
32
22
  }