@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
|
@@ -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({
|
|
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");
|
|
@@ -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">
|
|
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">
|
|
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">
|
|
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 &&
|
|
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 &&
|
|
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
|
|
41
|
+
const _archivedProduct = await StripeProductService.archiveProduct({ id: productToArchive.id });
|
|
42
42
|
setProductToArchive(null); // Close dialog on success
|
|
43
43
|
onProductsChange();
|
|
44
|
-
} catch (
|
|
45
|
-
console.error("[ProductsList] Failed to archive product:",
|
|
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
|
|
59
|
+
const _reactivatedProduct = await StripeProductService.reactivateProduct({ id: productToReactivate.id });
|
|
60
60
|
setProductToReactivate(null); // Close dialog on success
|
|
61
61
|
onProductsChange();
|
|
62
|
-
} catch (
|
|
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,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,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
|
+
});
|
package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx
CHANGED
|
@@ -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
|
|
121
|
+
<SubscriptionStatusBadge
|
|
122
|
+
status={subscription.status}
|
|
123
|
+
cancelAtPeriodEnd={subscription.cancelAtPeriodEnd}
|
|
124
|
+
/>
|
|
122
125
|
</div>
|
|
123
126
|
|
|
124
127
|
{/* Plan Info */}
|
package/src/features/billing/stripe-subscription/components/forms/CancelSubscriptionDialog.tsx
CHANGED
|
@@ -43,7 +43,7 @@ export function CancelSubscriptionDialog({
|
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (
|
|
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) : "
|
|
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
|
|
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({
|
|
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 && (
|
package/src/features/billing/stripe-subscription/components/widgets/SubscriptionStatusBadge.tsx
CHANGED
|
@@ -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
|
|
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
|
|
package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx
CHANGED
|
@@ -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
|
);
|
package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx
CHANGED
|
@@ -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
|
}
|