@carlonicora/nextjs-jsonapi 5.4.0 → 5.6.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-4YCAIEPT.js → BlockNoteEditor-AE3ZTEEH.js} +20 -20
- package/dist/{BlockNoteEditor-4YCAIEPT.js.map → BlockNoteEditor-AE3ZTEEH.js.map} +1 -1
- package/dist/{BlockNoteEditor-YQL2SGFR.mjs → BlockNoteEditor-NEYG5L6W.mjs} +5 -5
- package/dist/{BlockNoteViewer-AEJKJ27Q.mjs → BlockNoteViewer-3OUT5UFY.mjs} +3 -3
- package/dist/{BlockNoteViewer-YL3J7J2Y.js → BlockNoteViewer-4CFKQM7U.js} +4 -4
- package/dist/{BlockNoteViewer-YL3J7J2Y.js.map → BlockNoteViewer-4CFKQM7U.js.map} +1 -1
- package/dist/billing/index.d.mts +186 -25
- package/dist/billing/index.d.ts +186 -25
- package/dist/billing/index.js +1149 -588
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +786 -225
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-5PPG6PL7.js → chunk-3I5AAJXV.js} +3 -3
- package/dist/{chunk-5PPG6PL7.js.map → chunk-3I5AAJXV.js.map} +1 -1
- package/dist/{chunk-YW4ZCEWB.js → chunk-53BAULDP.js} +4 -4
- package/dist/{chunk-YW4ZCEWB.js.map → chunk-53BAULDP.js.map} +1 -1
- package/dist/{chunk-XCHSX3VC.mjs → chunk-BTPUYUQ3.mjs} +2 -2
- package/dist/{chunk-WC63PJSC.mjs → chunk-EYA2F6AN.mjs} +756 -24
- package/dist/chunk-EYA2F6AN.mjs.map +1 -0
- package/dist/{chunk-NFBOSPNJ.js → chunk-HKZAESS6.js} +954 -222
- package/dist/chunk-HKZAESS6.js.map +1 -0
- package/dist/{chunk-6DC2WVVB.mjs → chunk-KQHBONL4.mjs} +2 -2
- package/dist/{chunk-3NLAXVIN.js → chunk-LQ4DK4JF.js} +1988 -1089
- package/dist/chunk-LQ4DK4JF.js.map +1 -0
- package/dist/{chunk-YNCQSWQW.js → chunk-RL3PL3KV.js} +8 -8
- package/dist/{chunk-YNCQSWQW.js.map → chunk-RL3PL3KV.js.map} +1 -1
- package/dist/{chunk-EUKTBOI2.mjs → chunk-SFYPXCK4.mjs} +3382 -2483
- package/dist/chunk-SFYPXCK4.mjs.map +1 -0
- package/dist/{chunk-QMYQRPQC.mjs → chunk-Y7RR57EW.mjs} +2 -2
- package/dist/client/index.js +5 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -4
- package/dist/components/editors/server.d.mts +15 -0
- package/dist/components/editors/server.d.ts +15 -0
- package/dist/components/editors/server.js +38 -0
- package/dist/components/editors/server.js.map +1 -0
- package/dist/components/editors/server.mjs +38 -0
- package/dist/components/editors/server.mjs.map +1 -0
- package/dist/components/index.d.mts +33 -10
- package/dist/components/index.d.ts +33 -10
- package/dist/components/index.js +7 -5
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +8 -6
- package/dist/contexts/index.d.mts +9 -1
- package/dist/contexts/index.d.ts +9 -1
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +242 -3
- package/dist/core/index.d.ts +242 -3
- package/dist/core/index.js +36 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +35 -3
- package/dist/email-automations/index.d.mts +3 -0
- package/dist/email-automations/index.d.ts +3 -0
- package/dist/email-automations/index.js +99 -95
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +30 -26
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.d.mts +11 -4
- package/dist/features/analytics/index.d.ts +11 -4
- package/dist/features/analytics/index.js +127 -135
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +16 -24
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.d.mts +5 -2
- package/dist/features/analytics/server.d.ts +5 -2
- package/dist/features/analytics/server.js +7 -7
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +5 -5
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.d.mts +12 -2
- package/dist/features/blog/index.d.ts +12 -2
- package/dist/features/blog/index.js +206 -195
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +71 -60
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.d.mts +4 -1
- package/dist/features/blog/server.d.ts +4 -1
- package/dist/features/blog/server.js +49 -21
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +36 -8
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +38 -38
- package/dist/features/help/index.mjs +4 -4
- package/dist/features/tokenusage/index.d.mts +1 -3
- package/dist/features/tokenusage/index.d.ts +1 -3
- package/dist/features/tokenusage/index.js +127 -135
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +10 -18
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → format-options-BtX3o0Yu.d.ts} +104 -1
- package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → format-options-D6c7FNDi.d.mts} +104 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +36 -4
- package/dist/server/index.js +4 -4
- package/dist/server/index.mjs +3 -3
- package/messages/en.json +125 -3
- package/messages/it.json +125 -3
- package/package.json +9 -1
- package/scripts/validate-messages.mjs +1 -0
- package/src/billing/index.ts +1 -0
- package/src/client/index.ts +4 -0
- package/src/components/editors/server/BlockNoteServerContent.tsx +28 -0
- package/src/components/editors/server/__tests__/renderBlockNoteHTML.spec.ts +44 -0
- package/src/components/editors/server/renderBlockNoteHTML.ts +26 -0
- package/src/components/editors/server-entry.ts +7 -0
- package/src/components/index.ts +1 -0
- package/src/components/viewers/BlockNoteStaticViewer.tsx +44 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/administration/components/AdminIndexContainer.tsx +61 -33
- package/src/features/administration/components/__tests__/AdminIndexContainer.spec.tsx +61 -1
- package/src/features/administration/components/index.ts +2 -1
- package/src/features/administration/contexts/AdministrationContext.tsx +11 -3
- package/src/features/administration/contexts/__tests__/AdministrationContext.spec.tsx +40 -0
- package/src/features/ai-connection/ai-connection.module.ts +2 -0
- package/src/features/ai-connection/contexts/AiConnectionsContext.tsx +3 -16
- package/src/features/ai-connection/contexts/__tests__/AiConnectionsContext.spec.tsx +33 -0
- package/src/features/analytics/__tests__/server-entry.spec.tsx +2 -1
- package/src/features/analytics/analytics.modules.ts +22 -5
- package/src/features/analytics/components/AnalyticsAdminContainer.tsx +4 -3
- package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +6 -3
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +4 -3
- package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +11 -0
- package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +12 -1
- package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +6 -3
- package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +8 -17
- package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +17 -0
- package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
- package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
- package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
- package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
- package/src/features/billing/components/utils/discount.ts +38 -0
- package/src/features/billing/components/utils/index.ts +2 -0
- package/src/features/billing/components/utils/url.ts +12 -0
- package/src/features/billing/contexts/CouponContext.tsx +211 -0
- package/src/features/billing/contexts/index.ts +1 -0
- package/src/features/billing/i18n-keys.ts +79 -0
- package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
- package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +69 -0
- package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +79 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +192 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +333 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
- package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
- package/src/features/billing/stripe-coupon/components/index.ts +5 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
- package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
- package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
- package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +80 -0
- package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
- package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
- package/src/features/billing/stripe-coupon/data/index.ts +10 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +51 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +165 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +154 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
- package/src/features/billing/stripe-coupon/index.ts +5 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +14 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
- package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +18 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
- package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
- package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
- package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
- package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
- package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
- package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
- package/src/features/billing/stripe-price/stripe-price.module.ts +2 -0
- package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
- package/src/features/billing/stripe-product/stripe-product.module.ts +2 -0
- package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
- package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
- package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
- package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
- package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
- package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
- package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
- package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
- package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
- package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
- package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
- package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
- package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
- package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +54 -0
- package/src/features/blog/components/admin/BlogAdminContainer.tsx +13 -6
- package/src/features/blog/components/admin/BlogCategoriesList.tsx +1 -1
- package/src/features/blog/components/admin/BlogCoverImageField.tsx +1 -1
- package/src/features/blog/components/admin/BlogPostContainer.tsx +0 -1
- package/src/features/blog/components/admin/BlogPostEditor.tsx +42 -33
- package/src/features/blog/components/admin/BlogPostTranslationEditor.tsx +12 -4
- package/src/features/blog/components/admin/BlogPostsList.tsx +1 -1
- package/src/features/blog/components/public/BlogPostCard.tsx +2 -9
- package/src/features/blog/components/public/BlogPostMeta.tsx +2 -2
- package/src/features/blog/components/public/BlogPostView.tsx +9 -3
- package/src/features/blog/components/public/__tests__/BlogList.spec.tsx +2 -0
- package/src/features/blog/components/public/__tests__/BlogPostView.spec.tsx +17 -11
- package/src/features/blog/server/fetchers.ts +57 -8
- package/src/features/email-automation/components/containers/EmailAutomationsListContainer.tsx +12 -3
- package/src/features/rbac/contexts/RbacContext.tsx +4 -7
- package/src/features/rbac/contexts/__tests__/RbacContext.spec.tsx +42 -0
- package/src/features/tokenusage/components/TokenUsageAdminContainer.tsx +4 -3
- package/src/features/tokenusage/contexts/TokenUsageAdminContext.tsx +3 -18
- package/src/features/tokenusage/contexts/__tests__/TokenUsageAdminContext.spec.tsx +17 -0
- package/src/features/tokenusage/tokenusage-admin.module.ts +4 -3
- package/src/features/tokenusage/tokenusage.modules.ts +12 -2
- package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
- package/src/features/user/contexts/UserContext.tsx +18 -3
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
- package/src/features/user/contexts/__tests__/UserContext.spec.tsx +95 -0
- package/dist/chunk-3NLAXVIN.js.map +0 -1
- package/dist/chunk-EUKTBOI2.mjs.map +0 -1
- package/dist/chunk-NFBOSPNJ.js.map +0 -1
- package/dist/chunk-WC63PJSC.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-YQL2SGFR.mjs.map → BlockNoteEditor-NEYG5L6W.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-AEJKJ27Q.mjs.map → BlockNoteViewer-3OUT5UFY.mjs.map} +0 -0
- /package/dist/{chunk-XCHSX3VC.mjs.map → chunk-BTPUYUQ3.mjs.map} +0 -0
- /package/dist/{chunk-6DC2WVVB.mjs.map → chunk-KQHBONL4.mjs.map} +0 -0
- /package/dist/{chunk-QMYQRPQC.mjs.map → chunk-Y7RR57EW.mjs.map} +0 -0
|
@@ -4,7 +4,6 @@ import { useTranslations } from "next-intl";
|
|
|
4
4
|
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
5
5
|
import { Modules, NextRef } from "../../../core";
|
|
6
6
|
import { SharedProvider } from "../../../contexts";
|
|
7
|
-
import { usePageUrlGenerator } from "../../../hooks";
|
|
8
7
|
import { BreadcrumbItemData } from "../../../interfaces";
|
|
9
8
|
import { AnalyticsAdminFilterBar } from "../components/AnalyticsAdminFilterBar";
|
|
10
9
|
import type { AnalyticsBreakdownInterface } from "../data/analytics-breakdown.interface";
|
|
@@ -19,13 +18,6 @@ import {
|
|
|
19
18
|
} from "../data/analytics.types";
|
|
20
19
|
import { AnalyticsAdminService } from "../data/AnalyticsAdminService";
|
|
21
20
|
|
|
22
|
-
/**
|
|
23
|
-
* Default route the breadcrumb links back to. A host app that mounts the page
|
|
24
|
-
* elsewhere overrides it with the `pageUrl` prop — the constant stays here
|
|
25
|
-
* because the breadcrumb is built by this provider, not by the app.
|
|
26
|
-
*/
|
|
27
|
-
const ANALYTICS_ADMIN_PAGE_URL = "/administration/analytics";
|
|
28
|
-
|
|
29
21
|
/** Default base route of the journey page each session row links to. */
|
|
30
22
|
const ANALYTICS_SESSION_PAGE_URL = "/administration/analytics/sessions";
|
|
31
23
|
|
|
@@ -86,7 +78,11 @@ type AnalyticsAdminProviderProps = {
|
|
|
86
78
|
initialFrom?: string;
|
|
87
79
|
/** ISO 8601 instant. Defaults to now. */
|
|
88
80
|
initialTo?: string;
|
|
89
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* @deprecated No longer read. This provider only serves the dashboard, which
|
|
83
|
+
* is the current page, so its breadcrumb is not linked. Kept so existing
|
|
84
|
+
* callers still compile.
|
|
85
|
+
*/
|
|
90
86
|
pageUrl?: string;
|
|
91
87
|
/** Base route of the journey page each session row links to. */
|
|
92
88
|
sessionPageUrl?: string;
|
|
@@ -105,11 +101,9 @@ export const AnalyticsAdminProvider = ({
|
|
|
105
101
|
children,
|
|
106
102
|
initialFrom,
|
|
107
103
|
initialTo,
|
|
108
|
-
pageUrl = ANALYTICS_ADMIN_PAGE_URL,
|
|
109
104
|
sessionPageUrl = ANALYTICS_SESSION_PAGE_URL,
|
|
110
105
|
}: AnalyticsAdminProviderProps) => {
|
|
111
106
|
const t = useTranslations();
|
|
112
|
-
const generateUrl = usePageUrlGenerator();
|
|
113
107
|
|
|
114
108
|
const [filters, setFilterState] = useState<AnalyticsAdminFilterState>(() => {
|
|
115
109
|
const range = defaultRange();
|
|
@@ -205,12 +199,9 @@ export const AnalyticsAdminProvider = ({
|
|
|
205
199
|
}
|
|
206
200
|
}, [sessionsNextPage]);
|
|
207
201
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
href: generateUrl({ page: pageUrl }),
|
|
212
|
-
},
|
|
213
|
-
];
|
|
202
|
+
// Unlinked: this provider only serves the dashboard, so the root is always
|
|
203
|
+
// the current page. The journey page publishes its own linked breadcrumb.
|
|
204
|
+
const breadcrumb = (): BreadcrumbItemData[] => [{ name: t("analytics.admin.title") }];
|
|
214
205
|
|
|
215
206
|
const title = () => ({
|
|
216
207
|
type: t("analytics.admin.title"),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { useSharedContext } from "../../../../contexts/SharedContext";
|
|
3
4
|
import { AnalyticsAdminService } from "../../data/AnalyticsAdminService";
|
|
4
5
|
import { AnalyticsAdminProvider, useAnalyticsAdmin } from "../AnalyticsAdminContext";
|
|
5
6
|
|
|
@@ -40,6 +41,11 @@ function Probe() {
|
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function BreadcrumbProbe() {
|
|
45
|
+
const { breadcrumbs } = useSharedContext();
|
|
46
|
+
return <span data-testid="breadcrumbs">{JSON.stringify(breadcrumbs)}</span>;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
describe("AnalyticsAdminContext", () => {
|
|
44
50
|
// The mocked service is module-level, so its call counts survive across tests.
|
|
45
51
|
beforeEach(() => {
|
|
@@ -137,4 +143,15 @@ describe("AnalyticsAdminContext", () => {
|
|
|
137
143
|
);
|
|
138
144
|
expect(screen.getByTestId("has-more")).toHaveTextContent("false");
|
|
139
145
|
});
|
|
146
|
+
|
|
147
|
+
it("publishes the dashboard as an unlinked breadcrumb, since it is the current page", async () => {
|
|
148
|
+
render(
|
|
149
|
+
<AnalyticsAdminProvider>
|
|
150
|
+
<BreadcrumbProbe />
|
|
151
|
+
</AnalyticsAdminProvider>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await waitFor(() => expect(AnalyticsAdminService.getSessions).toHaveBeenCalled());
|
|
155
|
+
expect(screen.getByTestId("breadcrumbs")).toHaveTextContent(JSON.stringify([{ name: "analytics.admin.title" }]));
|
|
156
|
+
});
|
|
140
157
|
});
|
|
@@ -71,8 +71,9 @@ export function InvoicesSummaryCard({ invoices, loading, error, onViewAllClick }
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
//
|
|
75
|
-
|
|
74
|
+
// The most recent invoice that could charge something: a void one is an unpaid first-payment
|
|
75
|
+
// attempt that was replaced, which never charged the GM (fall back to it only when nothing else exists)
|
|
76
|
+
const latestInvoice = invoices.find((inv) => inv.status !== InvoiceStatus.VOID) ?? invoices[0];
|
|
76
77
|
const paidInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.PAID);
|
|
77
78
|
const openInvoices = invoices.filter((inv) => inv.status === InvoiceStatus.OPEN);
|
|
78
79
|
|
|
@@ -91,7 +91,8 @@ export function SubscriptionSummaryCard({
|
|
|
91
91
|
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
|
92
92
|
</CardHeader>
|
|
93
93
|
<CardContent>
|
|
94
|
-
{
|
|
94
|
+
{/* No active or trialing plan (none at all, or only unpaid / ended ones): offer the plans */}
|
|
95
|
+
{!primarySubscription ? (
|
|
95
96
|
<div className="space-y-2">
|
|
96
97
|
<p className="text-xl font-bold text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_title")}</p>
|
|
97
98
|
<p className="text-xs text-muted-foreground">{t("billing.ui.cards.subscriptions.empty_description")}</p>
|
|
@@ -108,7 +109,7 @@ export function SubscriptionSummaryCard({
|
|
|
108
109
|
<ChevronRight className="h-4 w-4 ms-1 rtl:rotate-180" />
|
|
109
110
|
</Button>
|
|
110
111
|
</div>
|
|
111
|
-
) :
|
|
112
|
+
) : (
|
|
112
113
|
<div className="space-y-2">
|
|
113
114
|
<div className="flex items-center gap-2">
|
|
114
115
|
<p className="text-xl font-bold">
|
|
@@ -147,7 +148,7 @@ export function SubscriptionSummaryCard({
|
|
|
147
148
|
</p>
|
|
148
149
|
)}
|
|
149
150
|
</div>
|
|
150
|
-
)
|
|
151
|
+
)}
|
|
151
152
|
</CardContent>
|
|
152
153
|
</Card>
|
|
153
154
|
);
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
SubscriptionSummaryCard,
|
|
23
23
|
} from "../cards";
|
|
24
24
|
import { BillingDetailModal } from "../modals/BillingDetailModal";
|
|
25
|
+
import { urlWithoutParam } from "../utils/url";
|
|
25
26
|
import { BillingAlertBanner } from "../widgets/BillingAlertBanner";
|
|
26
27
|
|
|
27
28
|
type ModalType = "subscriptions" | "payment-methods" | "invoices" | "usage" | null;
|
|
@@ -282,14 +283,27 @@ export function BillingDashboardContainer() {
|
|
|
282
283
|
fetchAllData();
|
|
283
284
|
}, [fetchAllData]);
|
|
284
285
|
|
|
285
|
-
// Handle URL action parameter for deep linking
|
|
286
|
+
// Handle URL action parameter for deep linking. `?action=subscribe` records the
|
|
287
|
+
// intent to subscribe; the wizard itself only renders once a customer exists, so a
|
|
288
|
+
// company without one sees the setup card first and the wizard opens right after
|
|
289
|
+
// "Set Up Billing Account" succeeds.
|
|
286
290
|
useEffect(() => {
|
|
287
291
|
const action = searchParams.get("action");
|
|
288
292
|
if (action === "subscribe") {
|
|
289
|
-
// Open wizard directly (no wrapper modal)
|
|
290
293
|
setShowWizard(true);
|
|
291
|
-
//
|
|
292
|
-
|
|
294
|
+
// Drop only `action`, so a refresh does not re-trigger it. Every other parameter
|
|
295
|
+
// must survive: a host page (the settings hub) selects this dashboard through
|
|
296
|
+
// `?section=`, and losing it switches the page away and unmounts this component.
|
|
297
|
+
window.history.replaceState(
|
|
298
|
+
window.history.state,
|
|
299
|
+
"",
|
|
300
|
+
urlWithoutParam({
|
|
301
|
+
pathname: window.location.pathname,
|
|
302
|
+
search: window.location.search,
|
|
303
|
+
hash: window.location.hash,
|
|
304
|
+
param: "action",
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
293
307
|
}
|
|
294
308
|
}, [searchParams]);
|
|
295
309
|
|
|
@@ -445,9 +459,12 @@ export function BillingDashboardContainer() {
|
|
|
445
459
|
onOpenChange={handleModalClose}
|
|
446
460
|
title={getModalTitle("subscriptions")}
|
|
447
461
|
>
|
|
462
|
+
{/* The dashboard's list, so a wizard success (refreshData) refreshes the dialog too */}
|
|
448
463
|
<SubscriptionsContainer
|
|
449
464
|
onOpenWizard={handleOpenWizard}
|
|
450
465
|
hasActiveRecurringSubscription={hasActiveRecurringSubscription}
|
|
466
|
+
subscriptions={data.subscriptions}
|
|
467
|
+
onSubscriptionsChange={refreshData}
|
|
451
468
|
/>
|
|
452
469
|
</BillingDetailModal>
|
|
453
470
|
|
|
@@ -477,7 +494,7 @@ export function BillingDashboardContainer() {
|
|
|
477
494
|
|
|
478
495
|
{/* Subscription Wizard - rendered at dashboard level to avoid nested dialogs */}
|
|
479
496
|
<SubscriptionWizard
|
|
480
|
-
open={showWizard}
|
|
497
|
+
open={showWizard && !!data.customer}
|
|
481
498
|
onOpenChange={(open) => !open && handleWizardClose()}
|
|
482
499
|
onSuccess={refreshData}
|
|
483
500
|
hasActiveRecurringSubscription={hasActiveRecurringSubscription}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { StripeSubscriptionInterface } from "../../../stripe-subscription/data/stripe-subscription.interface";
|
|
3
|
+
import { applyDiscount, promotionCodeDiscount, subscriptionDiscount } from "../discount";
|
|
4
|
+
|
|
5
|
+
const subscription = (discount: StripeSubscriptionInterface["discount"]) =>
|
|
6
|
+
({ discount }) as unknown as StripeSubscriptionInterface;
|
|
7
|
+
|
|
8
|
+
describe("applyDiscount", () => {
|
|
9
|
+
it("takes a percentage off", () => {
|
|
10
|
+
expect(applyDiscount(999, { discountType: "percent_off", discountValue: 20 })).toBeCloseTo(799.2);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("takes an amount off without going below zero", () => {
|
|
14
|
+
expect(applyDiscount(499, { discountType: "amount_off", discountValue: 100 })).toBe(399);
|
|
15
|
+
expect(applyDiscount(499, { discountType: "amount_off", discountValue: 1000 })).toBe(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns null without a discount or an amount", () => {
|
|
19
|
+
expect(applyDiscount(999, null)).toBeNull();
|
|
20
|
+
expect(applyDiscount(undefined, { discountType: "percent_off", discountValue: 20 })).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("promotionCodeDiscount", () => {
|
|
25
|
+
it("reads a valid code", () => {
|
|
26
|
+
expect(
|
|
27
|
+
promotionCodeDiscount({ valid: true, code: "FOUNDER", discountType: "percent_off", discountValue: 20 }),
|
|
28
|
+
).toEqual({ discountType: "percent_off", discountValue: 20, currency: undefined });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("ignores an invalid or missing code", () => {
|
|
32
|
+
expect(
|
|
33
|
+
promotionCodeDiscount({ valid: false, code: "X", discountType: "percent_off", discountValue: 20 }),
|
|
34
|
+
).toBeNull();
|
|
35
|
+
expect(promotionCodeDiscount(null)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("subscriptionDiscount", () => {
|
|
40
|
+
it("reads a percentage discount", () => {
|
|
41
|
+
expect(subscriptionDiscount(subscription({ percentOff: 20, duration: "forever" }))).toEqual({
|
|
42
|
+
discountType: "percent_off",
|
|
43
|
+
discountValue: 20,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reads an amount discount with its currency", () => {
|
|
48
|
+
expect(subscriptionDiscount(subscription({ amountOff: 200, currency: "eur", duration: "once" }))).toEqual({
|
|
49
|
+
discountType: "amount_off",
|
|
50
|
+
discountValue: 200,
|
|
51
|
+
currency: "eur",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns null when the subscription has no discount", () => {
|
|
56
|
+
expect(subscriptionDiscount(subscription(undefined))).toBeNull();
|
|
57
|
+
expect(subscriptionDiscount(undefined)).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { urlWithoutParam } from "../url";
|
|
3
|
+
|
|
4
|
+
describe("urlWithoutParam", () => {
|
|
5
|
+
it("keeps the section when it drops the action", () => {
|
|
6
|
+
expect(
|
|
7
|
+
urlWithoutParam({ pathname: "/settings", search: "?section=billing&action=subscribe", param: "action" }),
|
|
8
|
+
).toBe("/settings?section=billing");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("keeps the section when the action comes first", () => {
|
|
12
|
+
expect(
|
|
13
|
+
urlWithoutParam({ pathname: "/it/settings", search: "?action=subscribe§ion=billing", param: "action" }),
|
|
14
|
+
).toBe("/it/settings?section=billing");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns the bare path when the action was the only parameter", () => {
|
|
18
|
+
expect(urlWithoutParam({ pathname: "/billing", search: "?action=subscribe", param: "action" })).toBe("/billing");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("leaves a URL without the parameter unchanged, hash included", () => {
|
|
22
|
+
expect(
|
|
23
|
+
urlWithoutParam({ pathname: "/settings", search: "?section=billing", hash: "#plans", param: "action" }),
|
|
24
|
+
).toBe("/settings?section=billing#plans");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("accepts a search string without the leading question mark", () => {
|
|
28
|
+
expect(
|
|
29
|
+
urlWithoutParam({ pathname: "/settings", search: "section=billing&action=subscribe", param: "action" }),
|
|
30
|
+
).toBe("/settings?section=billing");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PromotionCodeValidationResult } from "../../stripe-promotion-code/data/stripe-promotion-code.interface";
|
|
2
|
+
import { StripeSubscriptionInterface } from "../../stripe-subscription/data/stripe-subscription.interface";
|
|
3
|
+
|
|
4
|
+
/** A discount as the billing screens apply it: a percentage, or an amount in minor units. */
|
|
5
|
+
export type BillingDiscount = {
|
|
6
|
+
discountType: "percent_off" | "amount_off";
|
|
7
|
+
discountValue: number;
|
|
8
|
+
currency?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** The discount a valid promotion code (typed or automatic) gives, or null. */
|
|
12
|
+
export function promotionCodeDiscount(code: PromotionCodeValidationResult | null | undefined): BillingDiscount | null {
|
|
13
|
+
if (!code?.valid || !code.discountType || !code.discountValue) return null;
|
|
14
|
+
return { discountType: code.discountType, discountValue: code.discountValue, currency: code.currency };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The discount a subscription already carries in Stripe, or null. */
|
|
18
|
+
export function subscriptionDiscount(
|
|
19
|
+
subscription: StripeSubscriptionInterface | null | undefined,
|
|
20
|
+
): BillingDiscount | null {
|
|
21
|
+
const discount = subscription?.discount;
|
|
22
|
+
if (!discount) return null;
|
|
23
|
+
if (discount.percentOff) return { discountType: "percent_off", discountValue: discount.percentOff };
|
|
24
|
+
if (discount.amountOff) {
|
|
25
|
+
return { discountType: "amount_off", discountValue: discount.amountOff, currency: discount.currency };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The amount after the discount, in the same minor units; null when there is no
|
|
32
|
+
* discount or nothing to discount. An amount discount never goes below zero.
|
|
33
|
+
*/
|
|
34
|
+
export function applyDiscount(amount: number | undefined, discount: BillingDiscount | null): number | null {
|
|
35
|
+
if (!discount || !amount) return null;
|
|
36
|
+
if (discount.discountType === "percent_off") return amount * (1 - discount.discountValue / 100);
|
|
37
|
+
return Math.max(0, amount - discount.discountValue);
|
|
38
|
+
}
|
|
@@ -2,3 +2,5 @@ export { formatCurrency, formatFixed, formatInterval } from "./currency";
|
|
|
2
2
|
export { formatDate } from "./date";
|
|
3
3
|
export type { BillingFormatOptions, BillingTranslator } from "./format-options";
|
|
4
4
|
export { useBillingFormatOptions } from "./useBillingFormatOptions";
|
|
5
|
+
export { applyDiscount, promotionCodeDiscount, subscriptionDiscount } from "./discount";
|
|
6
|
+
export type { BillingDiscount } from "./discount";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL to write back after consuming a one-shot query parameter (a deep-link
|
|
3
|
+
* `?action=`). Drops only `param`; every other parameter and the hash stay, so a
|
|
4
|
+
* host page that keys its state on the URL (the settings hub's `?section=`) keeps
|
|
5
|
+
* showing the same place.
|
|
6
|
+
*/
|
|
7
|
+
export function urlWithoutParam(params: { pathname: string; search: string; hash?: string; param: string }): string {
|
|
8
|
+
const query = new URLSearchParams(params.search);
|
|
9
|
+
query.delete(params.param);
|
|
10
|
+
const remaining = query.toString();
|
|
11
|
+
return `${params.pathname}${remaining ? `?${remaining}` : ""}${params.hash ?? ""}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
|
5
|
+
import { SharedProvider } from "../../../contexts/SharedContext";
|
|
6
|
+
import { JsonApiHydratedDataInterface, Modules, rehydrate } from "../../../core";
|
|
7
|
+
import { usePageUrlGenerator } from "../../../hooks";
|
|
8
|
+
import { BreadcrumbItemData } from "../../../interfaces";
|
|
9
|
+
import { getRoleId } from "../../../roles";
|
|
10
|
+
import { useCurrentUserContext } from "../../user/contexts";
|
|
11
|
+
import { CouponRefresher } from "../stripe-coupon/components/forms/CouponRefresher";
|
|
12
|
+
import CouponEditor from "../stripe-coupon/components/forms/CouponEditor";
|
|
13
|
+
import {
|
|
14
|
+
StripeCouponCodeFlagsInput,
|
|
15
|
+
StripeCouponCodeInput,
|
|
16
|
+
StripeCouponCodeInterface,
|
|
17
|
+
} from "../stripe-coupon/data/stripe-coupon-code.interface";
|
|
18
|
+
import { StripeCouponCodeService } from "../stripe-coupon/data/stripe-coupon-code.service";
|
|
19
|
+
import {
|
|
20
|
+
StripeCouponInput,
|
|
21
|
+
StripeCouponInterface,
|
|
22
|
+
StripeCouponRenameInput,
|
|
23
|
+
} from "../stripe-coupon/data/stripe-coupon.interface";
|
|
24
|
+
import { StripeCouponService } from "../stripe-coupon/data/stripe-coupon.service";
|
|
25
|
+
|
|
26
|
+
export interface CouponContextType {
|
|
27
|
+
coupon: StripeCouponInterface | undefined;
|
|
28
|
+
setCoupon: (value: StripeCouponInterface | undefined) => void;
|
|
29
|
+
reloadCoupon: () => Promise<void>;
|
|
30
|
+
createCoupon: (input: StripeCouponInput) => Promise<StripeCouponInterface>;
|
|
31
|
+
renameCoupon: (input: StripeCouponRenameInput) => Promise<StripeCouponInterface>;
|
|
32
|
+
refreshCounts: (id?: string) => Promise<void>;
|
|
33
|
+
createCode: (input: StripeCouponCodeInput) => Promise<StripeCouponCodeInterface>;
|
|
34
|
+
updateCodeFlags: (input: StripeCouponCodeFlagsInput) => Promise<StripeCouponCodeInterface>;
|
|
35
|
+
/** Bumped by every mutation. Mounted lists re-fetch on it. */
|
|
36
|
+
catalogueVersion: number;
|
|
37
|
+
refreshCatalogue: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CouponContext = createContext<CouponContextType | undefined>(undefined);
|
|
41
|
+
|
|
42
|
+
export type CouponProviderProps = {
|
|
43
|
+
children: ReactNode;
|
|
44
|
+
dehydratedCoupon?: JsonApiHydratedDataInterface;
|
|
45
|
+
/**
|
|
46
|
+
* Publish breadcrumbs + title into SharedProvider. Pass `false` when this
|
|
47
|
+
* provider is mounted INSIDE another entity's page (the Codes tab of the
|
|
48
|
+
* products page mounts CouponProvider that way): SharedProvider replaces its
|
|
49
|
+
* value for the subtree rather than merging (SharedContext.tsx:35-37), so a
|
|
50
|
+
* nested provider that publishes chrome would blank the host page's title.
|
|
51
|
+
*/
|
|
52
|
+
publishChrome?: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const defaultContextValue: CouponContextType = {
|
|
56
|
+
coupon: undefined,
|
|
57
|
+
setCoupon: () => {},
|
|
58
|
+
reloadCoupon: async () => {},
|
|
59
|
+
createCoupon: async () => {
|
|
60
|
+
throw new Error("createCoupon requires a CouponProvider");
|
|
61
|
+
},
|
|
62
|
+
renameCoupon: async () => {
|
|
63
|
+
throw new Error("renameCoupon requires a CouponProvider");
|
|
64
|
+
},
|
|
65
|
+
refreshCounts: async () => {},
|
|
66
|
+
createCode: async () => {
|
|
67
|
+
throw new Error("createCode requires a CouponProvider");
|
|
68
|
+
},
|
|
69
|
+
updateCodeFlags: async () => {
|
|
70
|
+
throw new Error("updateCodeFlags requires a CouponProvider");
|
|
71
|
+
},
|
|
72
|
+
catalogueVersion: 0,
|
|
73
|
+
refreshCatalogue: () => {},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const CouponProvider = ({ children, dehydratedCoupon, publishChrome = true }: CouponProviderProps) => {
|
|
77
|
+
const t = useTranslations();
|
|
78
|
+
const generateUrl = usePageUrlGenerator();
|
|
79
|
+
const { hasRole } = useCurrentUserContext();
|
|
80
|
+
|
|
81
|
+
const [coupon, setCoupon] = useState<StripeCouponInterface | undefined>(
|
|
82
|
+
dehydratedCoupon ? rehydrate<StripeCouponInterface>(Modules.StripeCoupon, dehydratedCoupon) : undefined,
|
|
83
|
+
);
|
|
84
|
+
const [catalogueVersion, setCatalogueVersion] = useState<number>(0);
|
|
85
|
+
|
|
86
|
+
const refreshCatalogue = useCallback(() => setCatalogueVersion((value) => value + 1), []);
|
|
87
|
+
|
|
88
|
+
const reloadCoupon = useCallback(async () => {
|
|
89
|
+
if (!coupon?.id) return;
|
|
90
|
+
setCoupon(await StripeCouponService.getCoupon({ id: coupon.id }));
|
|
91
|
+
}, [coupon?.id]);
|
|
92
|
+
|
|
93
|
+
const createCoupon = useCallback(
|
|
94
|
+
async (input: StripeCouponInput) => {
|
|
95
|
+
const saved = await StripeCouponService.createCoupon(input);
|
|
96
|
+
refreshCatalogue();
|
|
97
|
+
return saved;
|
|
98
|
+
},
|
|
99
|
+
[refreshCatalogue],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const renameCoupon = useCallback(
|
|
103
|
+
async (input: StripeCouponRenameInput) => {
|
|
104
|
+
const saved = await StripeCouponService.renameCoupon(input);
|
|
105
|
+
setCoupon(saved);
|
|
106
|
+
refreshCatalogue();
|
|
107
|
+
return saved;
|
|
108
|
+
},
|
|
109
|
+
[refreshCatalogue],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const refreshCounts = useCallback(
|
|
113
|
+
async (id?: string) => {
|
|
114
|
+
const target = id ?? coupon?.id;
|
|
115
|
+
if (!target) return;
|
|
116
|
+
const saved = await StripeCouponService.refreshCounts({ id: target });
|
|
117
|
+
if (coupon?.id === target) setCoupon(saved);
|
|
118
|
+
refreshCatalogue();
|
|
119
|
+
},
|
|
120
|
+
[coupon?.id, refreshCatalogue],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const createCode = useCallback(
|
|
124
|
+
async (input: StripeCouponCodeInput) => {
|
|
125
|
+
const saved = await StripeCouponCodeService.createCode(input);
|
|
126
|
+
await reloadCoupon();
|
|
127
|
+
refreshCatalogue();
|
|
128
|
+
return saved;
|
|
129
|
+
},
|
|
130
|
+
[reloadCoupon, refreshCatalogue],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const updateCodeFlags = useCallback(
|
|
134
|
+
async (input: StripeCouponCodeFlagsInput) => {
|
|
135
|
+
const saved = await StripeCouponCodeService.updateFlags(input);
|
|
136
|
+
await reloadCoupon();
|
|
137
|
+
refreshCatalogue();
|
|
138
|
+
return saved;
|
|
139
|
+
},
|
|
140
|
+
[reloadCoupon, refreshCatalogue],
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const value = useMemo<CouponContextType>(
|
|
144
|
+
() => ({
|
|
145
|
+
coupon,
|
|
146
|
+
setCoupon,
|
|
147
|
+
reloadCoupon,
|
|
148
|
+
createCoupon,
|
|
149
|
+
renameCoupon,
|
|
150
|
+
refreshCounts,
|
|
151
|
+
createCode,
|
|
152
|
+
updateCodeFlags,
|
|
153
|
+
catalogueVersion,
|
|
154
|
+
refreshCatalogue,
|
|
155
|
+
}),
|
|
156
|
+
[
|
|
157
|
+
coupon,
|
|
158
|
+
reloadCoupon,
|
|
159
|
+
createCoupon,
|
|
160
|
+
renameCoupon,
|
|
161
|
+
refreshCounts,
|
|
162
|
+
createCode,
|
|
163
|
+
updateCodeFlags,
|
|
164
|
+
catalogueVersion,
|
|
165
|
+
refreshCatalogue,
|
|
166
|
+
],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (!publishChrome) return <CouponContext.Provider value={value}>{children}</CouponContext.Provider>;
|
|
170
|
+
|
|
171
|
+
const breadcrumb = (): BreadcrumbItemData[] => {
|
|
172
|
+
const items: BreadcrumbItemData[] = [
|
|
173
|
+
{
|
|
174
|
+
name: t("billing.admin.products.tabs.codes"),
|
|
175
|
+
// Linked only when it is NOT the current page. The Codes list is a tab
|
|
176
|
+
// of the products page, so the link carries its `?section=`.
|
|
177
|
+
...(coupon
|
|
178
|
+
? { href: generateUrl({ page: Modules.StripeProduct, additionalParameters: { section: "codes" } }) }
|
|
179
|
+
: {}),
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
if (coupon) items.push({ name: coupon.name });
|
|
183
|
+
return items;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const title = () => {
|
|
187
|
+
const response: any = { type: t("billing.admin.coupons.title") };
|
|
188
|
+
if (!coupon) return response;
|
|
189
|
+
|
|
190
|
+
response.element = coupon.name;
|
|
191
|
+
|
|
192
|
+
// narr8 note: gate on hasRole ONLY. hasPermissionToModule always returns
|
|
193
|
+
// false in this app, so gating an editor on it hides the editor outright.
|
|
194
|
+
if (hasRole(getRoleId().Administrator)) {
|
|
195
|
+
response.functions = [
|
|
196
|
+
<CouponRefresher key="couponRefresher" coupon={coupon} />,
|
|
197
|
+
<CouponEditor key="couponEditor" coupon={coupon} propagateChanges={setCoupon} />,
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return response;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
|
|
206
|
+
<CouponContext.Provider value={value}>{children}</CouponContext.Provider>
|
|
207
|
+
</SharedProvider>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const useCouponContext = (): CouponContextType => useContext(CouponContext) ?? defaultContextValue;
|