@carlonicora/nextjs-jsonapi 6.4.0 → 6.5.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/billing/index.d.mts +174 -157
- package/dist/billing/index.d.ts +174 -157
- package/dist/billing/index.js +31 -9
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +31 -9
- package/dist/billing/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +3 -2
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +5 -0
- package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +44 -12
- package/src/features/billing/stripe-subscription/components/details/__tests__/SubscriptionDetails.spec.tsx +79 -0
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +9 -2
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@ import { PaymentMethodsContainer } from "../../stripe-customer/components";
|
|
|
10
10
|
import { StripeInvoiceInterface, StripeInvoiceService } from "../../stripe-invoice";
|
|
11
11
|
import { InvoicesContainer } from "../../stripe-invoice/components";
|
|
12
12
|
import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../../stripe-subscription";
|
|
13
|
-
import { SubscriptionsContainer } from "../../stripe-subscription/components";
|
|
13
|
+
import { type RenderCancelDialog, SubscriptionsContainer } from "../../stripe-subscription/components";
|
|
14
14
|
import { SubscriptionWizard } from "../../stripe-subscription/components/wizards";
|
|
15
15
|
import { MeterInterface, MeterSummaryInterface, StripeUsageService } from "../../stripe-usage";
|
|
16
16
|
import { UsageContainer } from "../../stripe-usage/components";
|
|
@@ -53,7 +53,7 @@ type ErrorState = {
|
|
|
53
53
|
usage: string | null;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export function BillingDashboardContainer() {
|
|
56
|
+
export function BillingDashboardContainer({ renderCancelDialog }: { renderCancelDialog?: RenderCancelDialog } = {}) {
|
|
57
57
|
const t = useTranslations();
|
|
58
58
|
const [data, setData] = useState<DataState>({
|
|
59
59
|
customer: null,
|
|
@@ -482,6 +482,7 @@ export function BillingDashboardContainer() {
|
|
|
482
482
|
hasActiveRecurringSubscription={hasActiveRecurringSubscription}
|
|
483
483
|
subscriptions={data.subscriptions}
|
|
484
484
|
onSubscriptionsChange={refreshData}
|
|
485
|
+
renderCancelDialog={renderCancelDialog}
|
|
485
486
|
/>
|
|
486
487
|
</BillingDetailModal>
|
|
487
488
|
|
package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
6
6
|
import { Button } from "../../../../../shadcnui";
|
|
7
7
|
import { BillingAlertBanner } from "../../../components";
|
|
8
8
|
import { StripeSubscriptionInterface, StripeSubscriptionService, SubscriptionStatus } from "../../data";
|
|
9
|
+
import type { RenderCancelDialog } from "../details/SubscriptionDetails";
|
|
9
10
|
import { SubscriptionsList } from "../lists";
|
|
10
11
|
|
|
11
12
|
type SubscriptionsContainerProps = {
|
|
@@ -18,6 +19,8 @@ type SubscriptionsContainerProps = {
|
|
|
18
19
|
*/
|
|
19
20
|
subscriptions?: StripeSubscriptionInterface[];
|
|
20
21
|
onSubscriptionsChange?: () => void;
|
|
22
|
+
/** Replaces the library cancel dialog; absent, the library dialog renders. */
|
|
23
|
+
renderCancelDialog?: RenderCancelDialog;
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export function SubscriptionsContainer({
|
|
@@ -25,6 +28,7 @@ export function SubscriptionsContainer({
|
|
|
25
28
|
hasActiveRecurringSubscription,
|
|
26
29
|
subscriptions: parentSubscriptions,
|
|
27
30
|
onSubscriptionsChange,
|
|
31
|
+
renderCancelDialog,
|
|
28
32
|
}: SubscriptionsContainerProps) {
|
|
29
33
|
const t = useTranslations();
|
|
30
34
|
const isControlled = parentSubscriptions !== undefined;
|
|
@@ -110,6 +114,7 @@ export function SubscriptionsContainer({
|
|
|
110
114
|
subscriptions={subscriptions}
|
|
111
115
|
onSubscriptionsChange={loadSubscriptions}
|
|
112
116
|
onChangePlan={(sub) => onOpenWizard?.(sub)}
|
|
117
|
+
renderCancelDialog={renderCancelDialog}
|
|
113
118
|
/>
|
|
114
119
|
)}
|
|
115
120
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from "next-intl";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { type ReactNode, useState } from "react";
|
|
5
5
|
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../../../../shadcnui";
|
|
6
6
|
import {
|
|
7
7
|
type BillingFormatOptions,
|
|
@@ -57,12 +57,26 @@ function formatBillingAmount(
|
|
|
57
57
|
return formatCurrency(price.unitAmount, price.currency, fmt);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Props an app receives when it replaces the library cancel dialog. */
|
|
61
|
+
export type CancelDialogRenderProps = {
|
|
62
|
+
subscription: StripeSubscriptionInterface;
|
|
63
|
+
open: boolean;
|
|
64
|
+
onOpenChange: (open: boolean) => void;
|
|
65
|
+
onSuccess: () => void;
|
|
66
|
+
/** Opens the plan-change wizard for this subscription and closes the details. Undefined when the host gave no onChangePlan. */
|
|
67
|
+
openChangePlan?: () => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Optional replacement for the library cancel dialog. Absent, the library renders CancelSubscriptionDialog. */
|
|
71
|
+
export type RenderCancelDialog = (props: CancelDialogRenderProps) => ReactNode;
|
|
72
|
+
|
|
60
73
|
type SubscriptionDetailsProps = {
|
|
61
74
|
subscription: StripeSubscriptionInterface;
|
|
62
75
|
open: boolean;
|
|
63
76
|
onOpenChange: (open: boolean) => void;
|
|
64
77
|
onSubscriptionChange: () => void;
|
|
65
78
|
onChangePlan?: (subscription: StripeSubscriptionInterface) => void;
|
|
79
|
+
renderCancelDialog?: RenderCancelDialog;
|
|
66
80
|
};
|
|
67
81
|
|
|
68
82
|
export function SubscriptionDetails({
|
|
@@ -71,6 +85,7 @@ export function SubscriptionDetails({
|
|
|
71
85
|
onOpenChange,
|
|
72
86
|
onSubscriptionChange,
|
|
73
87
|
onChangePlan,
|
|
88
|
+
renderCancelDialog,
|
|
74
89
|
}: SubscriptionDetailsProps) {
|
|
75
90
|
const t = useTranslations();
|
|
76
91
|
const fmt = useBillingFormatOptions();
|
|
@@ -175,17 +190,34 @@ export function SubscriptionDetails({
|
|
|
175
190
|
</Dialog>
|
|
176
191
|
|
|
177
192
|
{/* Cancel Subscription Dialog */}
|
|
178
|
-
{showCancel &&
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
193
|
+
{showCancel &&
|
|
194
|
+
(renderCancelDialog ? (
|
|
195
|
+
renderCancelDialog({
|
|
196
|
+
subscription,
|
|
197
|
+
open: showCancel,
|
|
198
|
+
onOpenChange: setShowCancel,
|
|
199
|
+
onSuccess: () => {
|
|
200
|
+
onSubscriptionChange();
|
|
201
|
+
setShowCancel(false);
|
|
202
|
+
},
|
|
203
|
+
openChangePlan: onChangePlan
|
|
204
|
+
? () => {
|
|
205
|
+
onChangePlan(subscription);
|
|
206
|
+
onOpenChange(false);
|
|
207
|
+
}
|
|
208
|
+
: undefined,
|
|
209
|
+
})
|
|
210
|
+
) : (
|
|
211
|
+
<CancelSubscriptionDialog
|
|
212
|
+
subscription={subscription}
|
|
213
|
+
open={showCancel}
|
|
214
|
+
onOpenChange={setShowCancel}
|
|
215
|
+
onSuccess={() => {
|
|
216
|
+
onSubscriptionChange();
|
|
217
|
+
setShowCancel(false);
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
))}
|
|
189
221
|
</>
|
|
190
222
|
);
|
|
191
223
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { StripeSubscriptionInterface, SubscriptionStatus } from "../../../data";
|
|
4
|
+
import { CancelDialogRenderProps, SubscriptionDetails } from "../SubscriptionDetails";
|
|
5
|
+
|
|
6
|
+
vi.mock("../../forms/CancelSubscriptionDialog", () => ({
|
|
7
|
+
CancelSubscriptionDialog: () => <div data-testid="default-cancel" />,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("../../../../stripe-customer", () => ({
|
|
11
|
+
StripeCustomerService: { createPortalSession: vi.fn() },
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
const sub = {
|
|
17
|
+
id: "sub-1",
|
|
18
|
+
status: SubscriptionStatus.ACTIVE,
|
|
19
|
+
cancelAtPeriodEnd: false,
|
|
20
|
+
currentPeriodStart: new Date("2026-10-01T00:00:00Z"),
|
|
21
|
+
currentPeriodEnd: new Date("2026-11-01T00:00:00Z"),
|
|
22
|
+
} as unknown as StripeSubscriptionInterface;
|
|
23
|
+
|
|
24
|
+
function renderDetails(props: Partial<Parameters<typeof SubscriptionDetails>[0]> = {}) {
|
|
25
|
+
const onOpenChange = vi.fn();
|
|
26
|
+
const onSubscriptionChange = vi.fn();
|
|
27
|
+
render(
|
|
28
|
+
<SubscriptionDetails
|
|
29
|
+
subscription={sub}
|
|
30
|
+
open
|
|
31
|
+
onOpenChange={onOpenChange}
|
|
32
|
+
onSubscriptionChange={onSubscriptionChange}
|
|
33
|
+
{...props}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
fireEvent.click(screen.getByText("ui.buttons.cancel"));
|
|
37
|
+
return { onOpenChange, onSubscriptionChange };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("SubscriptionDetails cancel dialog", () => {
|
|
41
|
+
it("renders the library dialog when no renderCancelDialog is given", () => {
|
|
42
|
+
renderDetails();
|
|
43
|
+
expect(screen.getByTestId("default-cancel")).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders the app dialog with the subscription when renderCancelDialog is given", () => {
|
|
47
|
+
const renderCancelDialog = vi.fn((_props: CancelDialogRenderProps) => <div data-testid="app-cancel" />);
|
|
48
|
+
renderDetails({ renderCancelDialog });
|
|
49
|
+
expect(screen.getByTestId("app-cancel")).toBeInTheDocument();
|
|
50
|
+
expect(screen.queryByTestId("default-cancel")).not.toBeInTheDocument();
|
|
51
|
+
const props = renderCancelDialog.mock.calls.at(-1)![0];
|
|
52
|
+
expect(props.subscription).toBe(sub);
|
|
53
|
+
expect(props.open).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("openChangePlan opens the wizard for this subscription and closes the details", () => {
|
|
57
|
+
const renderCancelDialog = vi.fn((_props: CancelDialogRenderProps) => <div data-testid="app-cancel" />);
|
|
58
|
+
const onChangePlan = vi.fn();
|
|
59
|
+
const { onOpenChange } = renderDetails({ renderCancelDialog, onChangePlan });
|
|
60
|
+
const props = renderCancelDialog.mock.calls.at(-1)![0];
|
|
61
|
+
expect(props.openChangePlan).toBeTypeOf("function");
|
|
62
|
+
props.openChangePlan!();
|
|
63
|
+
expect(onChangePlan).toHaveBeenCalledWith(sub);
|
|
64
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("openChangePlan is undefined when no onChangePlan is given", () => {
|
|
68
|
+
const renderCancelDialog = vi.fn((_props: CancelDialogRenderProps) => <div data-testid="app-cancel" />);
|
|
69
|
+
renderDetails({ renderCancelDialog });
|
|
70
|
+
expect(renderCancelDialog.mock.calls.at(-1)![0].openChangePlan).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("onSuccess refreshes the subscriptions", () => {
|
|
74
|
+
const renderCancelDialog = vi.fn((_props: CancelDialogRenderProps) => <div data-testid="app-cancel" />);
|
|
75
|
+
const { onSubscriptionChange } = renderDetails({ renderCancelDialog });
|
|
76
|
+
renderCancelDialog.mock.calls.at(-1)![0].onSuccess();
|
|
77
|
+
expect(onSubscriptionChange).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../../../components/utils";
|
|
13
13
|
import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
|
|
14
14
|
import { StripeSubscriptionInterface, SubscriptionStatus } from "../../data";
|
|
15
|
-
import { SubscriptionDetails } from "../details/SubscriptionDetails";
|
|
15
|
+
import { type RenderCancelDialog, SubscriptionDetails } from "../details/SubscriptionDetails";
|
|
16
16
|
import { SubscriptionStatusBadge } from "../widgets/SubscriptionStatusBadge";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -49,9 +49,15 @@ type SubscriptionsListProps = {
|
|
|
49
49
|
subscriptions: StripeSubscriptionInterface[];
|
|
50
50
|
onSubscriptionsChange: () => void;
|
|
51
51
|
onChangePlan?: (subscription: StripeSubscriptionInterface) => void;
|
|
52
|
+
renderCancelDialog?: RenderCancelDialog;
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
export function SubscriptionsList({
|
|
55
|
+
export function SubscriptionsList({
|
|
56
|
+
subscriptions,
|
|
57
|
+
onSubscriptionsChange,
|
|
58
|
+
onChangePlan,
|
|
59
|
+
renderCancelDialog,
|
|
60
|
+
}: SubscriptionsListProps) {
|
|
55
61
|
const t = useTranslations();
|
|
56
62
|
const fmt = useBillingFormatOptions();
|
|
57
63
|
const [selectedSub, setSelectedSub] = useState<StripeSubscriptionInterface | null>(null);
|
|
@@ -148,6 +154,7 @@ export function SubscriptionsList({ subscriptions, onSubscriptionsChange, onChan
|
|
|
148
154
|
}
|
|
149
155
|
: undefined
|
|
150
156
|
}
|
|
157
|
+
renderCancelDialog={renderCancelDialog}
|
|
151
158
|
/>
|
|
152
159
|
)}
|
|
153
160
|
</>
|