@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.
- package/dist/{BlockNoteEditor-4MDHRUS2.js → BlockNoteEditor-3S2B36O3.js} +15 -15
- package/dist/{BlockNoteEditor-4MDHRUS2.js.map → BlockNoteEditor-3S2B36O3.js.map} +1 -1
- package/dist/{BlockNoteEditor-SZWO3MDO.mjs → BlockNoteEditor-WQUJTVJL.mjs} +5 -5
- package/dist/BlockNoteEditor-WQUJTVJL.mjs.map +1 -0
- package/dist/billing/index.d.mts +15 -5
- package/dist/billing/index.d.ts +15 -5
- package/dist/billing/index.js +750 -520
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +665 -435
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-53IPQJVH.js → chunk-3EZX4G2E.js} +147 -23
- package/dist/chunk-3EZX4G2E.js.map +1 -0
- package/dist/{chunk-I7DFEJFF.mjs → chunk-4PHADEKA.mjs} +738 -1418
- package/dist/chunk-4PHADEKA.mjs.map +1 -0
- package/dist/{chunk-E6PQQTWF.js → chunk-T2JCZYWK.js} +999 -1679
- package/dist/chunk-T2JCZYWK.js.map +1 -0
- package/dist/{chunk-P7R2DPD6.mjs → chunk-TQ5GRRTM.mjs} +125 -1
- package/dist/chunk-TQ5GRRTM.mjs.map +1 -0
- package/dist/client/index.js +3 -3
- package/dist/client/index.mjs +2 -2
- package/dist/components/index.d.mts +23 -8
- package/dist/components/index.d.ts +23 -8
- package/dist/components/index.js +3 -3
- package/dist/components/index.mjs +2 -2
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +3 -3
- package/dist/contexts/index.mjs +2 -2
- package/dist/core/index.d.mts +47 -3
- package/dist/core/index.d.ts +47 -3
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7 -1
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{stripe-subscription.interface-DK7BJaNd.d.ts → stripe-promotion-code.interface-BcJty0rv.d.ts} +18 -1
- package/dist/{stripe-subscription.interface-C8uhCYIZ.d.mts → stripe-promotion-code.interface-Dnm2DJKQ.d.mts} +18 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/billing/index.ts +1 -0
- package/src/client/context/JsonApiProvider.tsx +1 -5
- package/src/client/hooks/__tests__/useJsonApiGet.test.tsx +9 -9
- package/src/client/hooks/__tests__/useJsonApiMutation.test.tsx +11 -11
- package/src/client/hooks/__tests__/useRehydration.test.ts +13 -34
- package/src/components/editors/BlockNoteEditor.tsx +2 -2
- package/src/components/forms/CommonEditorTrigger.tsx +1 -1
- package/src/components/forms/FormCheckbox.tsx +2 -12
- package/src/components/forms/FormDate.tsx +1 -6
- package/src/components/forms/FormInput.tsx +1 -1
- package/src/components/forms/FormPassword.tsx +1 -7
- package/src/components/forms/FormSelect.tsx +2 -8
- package/src/components/forms/FormSlider.tsx +1 -5
- package/src/components/forms/FormSwitch.tsx +1 -5
- package/src/components/forms/GdprConsentCheckbox.tsx +2 -8
- package/src/components/forms/PasswordInput.tsx +28 -26
- package/src/components/forms/__tests__/FormCheckbox.test.tsx +16 -18
- package/src/components/forms/__tests__/FormDate.test.tsx +14 -30
- package/src/components/forms/__tests__/FormInput.test.tsx +21 -37
- package/src/components/forms/__tests__/FormSelect.test.tsx +15 -21
- package/src/components/tables/ContentListTable.tsx +1 -1
- package/src/components/tables/__tests__/ContentListTable.test.tsx +17 -89
- package/src/components/tables/cells/cell.component.tsx +1 -1
- package/src/contexts/HeaderChildrenContext.tsx +3 -1
- package/src/core/endpoint/__tests__/EndpointCreator.test.ts +2 -7
- package/src/core/factories/__tests__/JsonApiDataFactory.test.ts +3 -3
- package/src/core/factories/__tests__/RehydrationFactory.test.ts +4 -6
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +1 -0
- package/src/core/registry/__tests__/DataClassRegistry.test.ts +5 -15
- package/src/core/registry/__tests__/ModuleRegistrar.test.ts +5 -15
- package/src/features/auth/components/GdprConsentSection.tsx +1 -6
- package/src/features/auth/components/details/LandingComponent.tsx +6 -1
- package/src/features/auth/components/forms/AcceptInvitation.tsx +1 -1
- package/src/features/auth/components/forms/ResetPassword.tsx +1 -1
- package/src/features/billing/components/cards/PaymentMethodSummaryCard.tsx +13 -18
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +12 -17
- package/src/features/billing/components/modals/BillingDetailModal.tsx +2 -13
- package/src/features/billing/stripe-customer/components/details/PaymentMethodCard.tsx +8 -1
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +2 -13
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +2 -12
- package/src/features/billing/stripe-invoice/components/details/InvoiceDetails.tsx +6 -1
- package/src/features/billing/stripe-invoice/data/stripe-invoice.interface.ts +1 -0
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +13 -5
- package/src/features/billing/stripe-product/components/lists/ProductsList.tsx +5 -5
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +108 -0
- package/src/features/billing/stripe-promotion-code/components/index.ts +1 -0
- package/src/features/billing/stripe-promotion-code/data/index.ts +3 -0
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +14 -0
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.service.ts +64 -0
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.ts +66 -0
- package/src/features/billing/stripe-promotion-code/index.ts +2 -0
- package/src/features/billing/stripe-promotion-code/stripe-promotion-code.module.ts +9 -0
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +1 -3
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +4 -1
- package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +24 -4
- package/src/features/billing/stripe-subscription/components/widgets/PricingCard.tsx +9 -2
- package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx +3 -1
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +7 -7
- package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +2 -10
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +3 -13
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +134 -23
- package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +2 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +8 -0
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +93 -7
- package/src/features/billing/stripe-usage/components/details/UsageSummaryCard.tsx +1 -1
- package/src/features/billing/stripe-usage/components/lists/UsageHistoryTable.tsx +1 -1
- package/src/features/company/components/details/CompanyDetails.tsx +2 -2
- package/src/features/company/components/forms/CompanyConfigurationSecurityForm.tsx +1 -1
- package/src/features/index.ts +1 -0
- package/src/features/notification/components/containers/NotificationsListContainer.tsx +1 -1
- package/src/features/notification/components/modals/NotificationModal.tsx +6 -2
- package/src/features/notification/contexts/NotificationContext.tsx +1 -3
- package/src/features/oauth/components/OAuthClientCard.tsx +15 -17
- package/src/features/oauth/components/OAuthClientDetail.tsx +7 -19
- package/src/features/oauth/components/OAuthClientForm.tsx +4 -13
- package/src/features/oauth/components/OAuthClientSecretDisplay.tsx +4 -20
- package/src/features/oauth/components/OAuthRedirectUriInput.tsx +5 -12
- package/src/features/oauth/components/OAuthScopeSelector.tsx +17 -23
- package/src/features/oauth/components/consent/OAuthConsentActions.tsx +3 -16
- package/src/features/oauth/components/consent/OAuthConsentHeader.tsx +3 -12
- package/src/features/oauth/components/consent/OAuthConsentScreen.tsx +5 -20
- package/src/features/oauth/components/consent/OAuthScopeList.tsx +3 -18
- package/src/features/onboarding/contexts/OnboardingContext.tsx +3 -3
- package/src/features/role/components/forms/FormRoles.tsx +1 -7
- package/src/features/user/components/containers/UserContainer.tsx +1 -1
- package/src/features/user/components/details/UserDetails.tsx +1 -1
- package/src/features/user/components/forms/UserDeleter.tsx +1 -1
- package/src/features/user/components/forms/UserEditor.tsx +1 -1
- package/src/features/user/components/forms/UserMultiSelect.tsx +7 -7
- package/src/features/user/components/lists/UserListInAdd.tsx +2 -2
- package/src/features/user/components/lists/UsersList.tsx +7 -1
- package/src/features/user/contexts/CurrentUserContext.tsx +36 -33
- package/src/hooks/__tests__/useDataListRetriever.test.ts +15 -21
- package/src/hooks/__tests__/useDebounce.test.ts +2 -7
- package/src/hooks/useCustomD3Graph.tsx +2 -2
- package/src/shadcnui/custom/multi-select.tsx +28 -2
- package/src/shadcnui/ui/accordion.tsx +21 -23
- package/src/shadcnui/ui/alert-dialog.tsx +45 -62
- package/src/shadcnui/ui/alert.tsx +25 -41
- package/src/shadcnui/ui/avatar.tsx +23 -36
- package/src/shadcnui/ui/badge.tsx +13 -11
- package/src/shadcnui/ui/breadcrumb.tsx +21 -55
- package/src/shadcnui/ui/button.tsx +17 -18
- package/src/shadcnui/ui/calendar.tsx +44 -93
- package/src/shadcnui/ui/carousel.tsx +72 -100
- package/src/shadcnui/ui/chart.tsx +102 -161
- package/src/shadcnui/ui/checkbox.tsx +8 -9
- package/src/shadcnui/ui/combobox.tsx +52 -83
- package/src/shadcnui/ui/command.tsx +43 -77
- package/src/shadcnui/ui/context-menu.tsx +47 -86
- package/src/shadcnui/ui/dialog.tsx +34 -60
- package/src/shadcnui/ui/drawer.tsx +32 -53
- package/src/shadcnui/ui/dropdown-menu.tsx +48 -65
- package/src/shadcnui/ui/field.tsx +39 -48
- package/src/shadcnui/ui/hover-card.tsx +9 -14
- package/src/shadcnui/ui/input-group.tsx +44 -55
- package/src/shadcnui/ui/input-otp.tsx +22 -26
- package/src/shadcnui/ui/input.tsx +6 -6
- package/src/shadcnui/ui/label.tsx +6 -6
- package/src/shadcnui/ui/navigation-menu.tsx +36 -60
- package/src/shadcnui/ui/popover.tsx +15 -38
- package/src/shadcnui/ui/progress.tsx +12 -29
- package/src/shadcnui/ui/radio-group.tsx +9 -15
- package/src/shadcnui/ui/resizable.tsx +14 -24
- package/src/shadcnui/ui/scroll-area.tsx +12 -27
- package/src/shadcnui/ui/select.tsx +41 -65
- package/src/shadcnui/ui/separator.tsx +7 -11
- package/src/shadcnui/ui/sheet.tsx +30 -55
- package/src/shadcnui/ui/sidebar.tsx +141 -189
- package/src/shadcnui/ui/skeleton.tsx +3 -9
- package/src/shadcnui/ui/slider.tsx +11 -23
- package/src/shadcnui/ui/switch.tsx +8 -8
- package/src/shadcnui/ui/tabs.tsx +14 -21
- package/src/shadcnui/ui/textarea.tsx +5 -5
- package/src/shadcnui/ui/toggle.tsx +8 -14
- package/src/shadcnui/ui/tooltip.tsx +11 -23
- package/src/testing/providers/MockJsonApiProvider.tsx +1 -5
- package/src/testing/utils/renderWithProviders.tsx +6 -10
- package/dist/BlockNoteEditor-SZWO3MDO.mjs.map +0 -1
- package/dist/chunk-53IPQJVH.js.map +0 -1
- package/dist/chunk-E6PQQTWF.js.map +0 -1
- package/dist/chunk-I7DFEJFF.mjs.map +0 -1
- 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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -9,9 +9,9 @@ import { usePageUrlGenerator } from "../../../../hooks";
|
|
|
9
9
|
import { useCompanyContext } from "../../contexts/CompanyContext";
|
|
10
10
|
|
|
11
11
|
export function CompanyDetails() {
|
|
12
|
-
const
|
|
12
|
+
const _t = useTranslations();
|
|
13
13
|
const { title } = useSharedContext();
|
|
14
|
-
const
|
|
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
|
|
23
|
+
const _t = useTranslations();
|
|
24
24
|
|
|
25
25
|
const renderProviderFields = () => {
|
|
26
26
|
const config = providerConfig;
|
package/src/features/index.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 =
|
|
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
|
|
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) => {
|
|
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">
|