@carlonicora/nextjs-jsonapi 5.5.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-THBJH2UR.js → BlockNoteEditor-AE3ZTEEH.js} +19 -19
- package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-AE3ZTEEH.js.map} +1 -1
- package/dist/{BlockNoteEditor-GVQCKRMU.mjs → BlockNoteEditor-NEYG5L6W.mjs} +4 -4
- package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-3OUT5UFY.mjs} +2 -2
- package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-4CFKQM7U.js} +3 -3
- package/dist/{BlockNoteViewer-VZOVKTFO.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 +1148 -587
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +785 -224
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-WPYT4UKJ.js → chunk-3I5AAJXV.js} +3 -3
- package/dist/{chunk-WPYT4UKJ.js.map → chunk-3I5AAJXV.js.map} +1 -1
- package/dist/{chunk-YDYVJM4I.js → chunk-53BAULDP.js} +4 -4
- package/dist/{chunk-YDYVJM4I.js.map → chunk-53BAULDP.js.map} +1 -1
- package/dist/{chunk-3CGROPF3.mjs → chunk-BTPUYUQ3.mjs} +2 -2
- package/dist/{chunk-VC5A3AUZ.mjs → chunk-EYA2F6AN.mjs} +696 -4
- package/dist/chunk-EYA2F6AN.mjs.map +1 -0
- package/dist/{chunk-G6UE6MEP.js → chunk-HKZAESS6.js} +896 -204
- package/dist/chunk-HKZAESS6.js.map +1 -0
- package/dist/{chunk-W7F5BDSV.mjs → chunk-KQHBONL4.mjs} +2 -2
- package/dist/{chunk-TY5FYP3G.js → chunk-LQ4DK4JF.js} +1919 -1056
- package/dist/chunk-LQ4DK4JF.js.map +1 -0
- package/dist/{chunk-S4YJ5WCW.js → chunk-RL3PL3KV.js} +8 -8
- package/dist/{chunk-S4YJ5WCW.js.map → chunk-RL3PL3KV.js.map} +1 -1
- package/dist/{chunk-X5WIUUNG.mjs → chunk-SFYPXCK4.mjs} +3237 -2374
- package/dist/chunk-SFYPXCK4.mjs.map +1 -0
- package/dist/{chunk-6SQYXUVP.mjs → chunk-Y7RR57EW.mjs} +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.js +4 -4
- package/dist/components/index.mjs +3 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +242 -3
- package/dist/core/index.d.ts +242 -3
- package/dist/core/index.js +34 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +33 -1
- package/dist/email-automations/index.js +92 -92
- package/dist/email-automations/index.mjs +3 -3
- package/dist/features/analytics/index.js +121 -121
- package/dist/features/analytics/index.mjs +4 -4
- package/dist/features/analytics/server.js +4 -4
- package/dist/features/analytics/server.mjs +2 -2
- package/dist/features/blog/index.js +150 -150
- package/dist/features/blog/index.mjs +3 -3
- package/dist/features/blog/server.js +16 -16
- package/dist/features/blog/server.mjs +1 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +124 -124
- package/dist/features/tokenusage/index.mjs +4 -4
- 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 +35 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/messages/en.json +125 -3
- package/messages/it.json +125 -3
- package/package.json +1 -1
- package/scripts/validate-messages.mjs +1 -0
- package/src/billing/index.ts +1 -0
- package/src/client/index.ts +4 -0
- package/src/core/index.ts +1 -0
- package/src/core/registry/ModuleRegistry.ts +3 -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-product/components/containers/ProductsListContainer.tsx +21 -6
- 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/user/contexts/CurrentUserContext.tsx +25 -0
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
- package/dist/chunk-G6UE6MEP.js.map +0 -1
- package/dist/chunk-TY5FYP3G.js.map +0 -1
- package/dist/chunk-VC5A3AUZ.mjs.map +0 -1
- package/dist/chunk-X5WIUUNG.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-GVQCKRMU.mjs.map → BlockNoteEditor-NEYG5L6W.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-3OUT5UFY.mjs.map} +0 -0
- /package/dist/{chunk-3CGROPF3.mjs.map → chunk-BTPUYUQ3.mjs.map} +0 -0
- /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-KQHBONL4.mjs.map} +0 -0
- /package/dist/{chunk-6SQYXUVP.mjs.map → chunk-Y7RR57EW.mjs.map} +0 -0
|
@@ -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;
|
|
@@ -13,6 +13,8 @@ export const BILLING_ADMIN_I18N_KEYS = [
|
|
|
13
13
|
"billing.admin.products.entity",
|
|
14
14
|
"billing.admin.products.create",
|
|
15
15
|
"billing.admin.products.tabs.details",
|
|
16
|
+
"billing.admin.products.tabs.products",
|
|
17
|
+
"billing.admin.products.tabs.codes",
|
|
16
18
|
"billing.admin.products.empty.title",
|
|
17
19
|
"billing.admin.products.empty.description",
|
|
18
20
|
"billing.admin.products.fields.name",
|
|
@@ -92,6 +94,74 @@ export const BILLING_ADMIN_I18N_KEYS = [
|
|
|
92
94
|
"billing.admin.prices.restore.title",
|
|
93
95
|
"billing.admin.prices.restore.description",
|
|
94
96
|
"billing.admin.prices.restore.confirm",
|
|
97
|
+
"billing.admin.coupons.all_products",
|
|
98
|
+
"billing.admin.coupons.codes.automatic",
|
|
99
|
+
"billing.admin.coupons.codes.empty.description",
|
|
100
|
+
"billing.admin.coupons.codes.empty.title",
|
|
101
|
+
"billing.admin.coupons.codes.entity",
|
|
102
|
+
"billing.admin.coupons.codes.never",
|
|
103
|
+
"billing.admin.coupons.codes.title",
|
|
104
|
+
"billing.admin.coupons.discount.amount",
|
|
105
|
+
"billing.admin.coupons.discount.forever",
|
|
106
|
+
"billing.admin.coupons.discount.once",
|
|
107
|
+
"billing.admin.coupons.discount.percent",
|
|
108
|
+
"billing.admin.coupons.discount.repeating",
|
|
109
|
+
"billing.admin.coupons.duration.forever",
|
|
110
|
+
"billing.admin.coupons.duration.once",
|
|
111
|
+
"billing.admin.coupons.duration.repeating",
|
|
112
|
+
"billing.admin.coupons.empty.description",
|
|
113
|
+
"billing.admin.coupons.empty.title",
|
|
114
|
+
"billing.admin.coupons.entity",
|
|
115
|
+
"billing.admin.coupons.errors.amountOff",
|
|
116
|
+
"billing.admin.coupons.errors.durationInMonths",
|
|
117
|
+
"billing.admin.coupons.errors.minimumAmount",
|
|
118
|
+
"billing.admin.coupons.errors.name",
|
|
119
|
+
"billing.admin.coupons.errors.percentOff",
|
|
120
|
+
"billing.admin.coupons.fields.amountOff",
|
|
121
|
+
"billing.admin.coupons.fields.automatic",
|
|
122
|
+
"billing.admin.coupons.fields.code",
|
|
123
|
+
"billing.admin.coupons.fields.codeMaxRedemptions",
|
|
124
|
+
"billing.admin.coupons.fields.codes",
|
|
125
|
+
"billing.admin.coupons.fields.currency",
|
|
126
|
+
"billing.admin.coupons.fields.discount",
|
|
127
|
+
"billing.admin.coupons.fields.discountType",
|
|
128
|
+
"billing.admin.coupons.fields.duration",
|
|
129
|
+
"billing.admin.coupons.fields.durationInMonths",
|
|
130
|
+
"billing.admin.coupons.fields.expiresAt",
|
|
131
|
+
"billing.admin.coupons.fields.first_time_warning",
|
|
132
|
+
"billing.admin.coupons.fields.firstTimeTransaction",
|
|
133
|
+
"billing.admin.coupons.fields.maxRedemptions",
|
|
134
|
+
"billing.admin.coupons.fields.minimumAmount",
|
|
135
|
+
"billing.admin.coupons.fields.name",
|
|
136
|
+
"billing.admin.coupons.fields.percentOff",
|
|
137
|
+
"billing.admin.coupons.fields.places",
|
|
138
|
+
"billing.admin.coupons.fields.products",
|
|
139
|
+
"billing.admin.coupons.fields.redeemBy",
|
|
140
|
+
"billing.admin.coupons.fields.status",
|
|
141
|
+
"billing.admin.coupons.fields.stripeCouponId",
|
|
142
|
+
"billing.admin.coupons.fields.stripeCustomerId",
|
|
143
|
+
"billing.admin.coupons.fields.stripeId",
|
|
144
|
+
"billing.admin.coupons.first_code",
|
|
145
|
+
"billing.admin.coupons.fixed_notice",
|
|
146
|
+
"billing.admin.coupons.help.automatic",
|
|
147
|
+
"billing.admin.coupons.help.products",
|
|
148
|
+
"billing.admin.coupons.placeholders.amountOff",
|
|
149
|
+
"billing.admin.coupons.placeholders.code",
|
|
150
|
+
"billing.admin.coupons.placeholders.durationInMonths",
|
|
151
|
+
"billing.admin.coupons.placeholders.maxRedemptions",
|
|
152
|
+
"billing.admin.coupons.placeholders.minimumAmount",
|
|
153
|
+
"billing.admin.coupons.placeholders.name",
|
|
154
|
+
"billing.admin.coupons.placeholders.percentOff",
|
|
155
|
+
"billing.admin.coupons.placeholders.stripeCouponId",
|
|
156
|
+
"billing.admin.coupons.places.limited",
|
|
157
|
+
"billing.admin.coupons.places.no_limit",
|
|
158
|
+
"billing.admin.coupons.places.unlimited",
|
|
159
|
+
"billing.admin.coupons.refresh.title",
|
|
160
|
+
"billing.admin.coupons.status.active",
|
|
161
|
+
"billing.admin.coupons.status.ended",
|
|
162
|
+
"billing.admin.coupons.title",
|
|
163
|
+
"billing.admin.coupons.type.amount_off",
|
|
164
|
+
"billing.admin.coupons.type.percent_off",
|
|
95
165
|
] as const;
|
|
96
166
|
|
|
97
167
|
/**
|
|
@@ -242,10 +312,12 @@ export const BILLING_UI_I18N_KEYS = [
|
|
|
242
312
|
"billing.ui.pricing.selected",
|
|
243
313
|
"billing.ui.pricing.standard",
|
|
244
314
|
"billing.ui.promo_code.apply",
|
|
315
|
+
"billing.ui.promo_code.automatic_label",
|
|
245
316
|
"billing.ui.promo_code.duration_forever",
|
|
246
317
|
"billing.ui.promo_code.duration_once",
|
|
247
318
|
"billing.ui.promo_code.duration_repeating",
|
|
248
319
|
"billing.ui.promo_code.placeholder",
|
|
320
|
+
"billing.ui.promo_code.use_different",
|
|
249
321
|
"billing.ui.proration.net_due_today",
|
|
250
322
|
"billing.ui.proration.next_invoice",
|
|
251
323
|
"billing.ui.proration.title",
|
|
@@ -289,8 +361,11 @@ export const BILLING_UI_I18N_KEYS = [
|
|
|
289
361
|
"billing.ui.wizard.addons_description",
|
|
290
362
|
"billing.ui.wizard.back",
|
|
291
363
|
"billing.ui.wizard.errors.already_subscribed",
|
|
364
|
+
"billing.ui.wizard.errors.discount_sold_out",
|
|
365
|
+
"billing.ui.wizard.errors.discount_unavailable",
|
|
292
366
|
"billing.ui.wizard.errors.invalid_promo_code",
|
|
293
367
|
"billing.ui.wizard.errors.payment_method_required",
|
|
368
|
+
"billing.ui.wizard.errors.payment_not_confirmed",
|
|
294
369
|
"billing.ui.wizard.errors.prepare_review",
|
|
295
370
|
"billing.ui.wizard.errors.process_subscription",
|
|
296
371
|
"billing.ui.wizard.errors.validate_promo_code",
|
|
@@ -299,8 +374,10 @@ export const BILLING_UI_I18N_KEYS = [
|
|
|
299
374
|
"billing.ui.wizard.next_review",
|
|
300
375
|
"billing.ui.wizard.payment_description",
|
|
301
376
|
"billing.ui.wizard.review.amount_off",
|
|
377
|
+
"billing.ui.wizard.review.card_on_file",
|
|
302
378
|
"billing.ui.wizard.review.charge_now",
|
|
303
379
|
"billing.ui.wizard.review.confirm_change",
|
|
380
|
+
"billing.ui.wizard.review.discount_carries_over",
|
|
304
381
|
"billing.ui.wizard.review.due_now",
|
|
305
382
|
"billing.ui.wizard.review.interval_monthly",
|
|
306
383
|
"billing.ui.wizard.review.interval_one_time",
|
|
@@ -317,6 +394,8 @@ export const BILLING_UI_I18N_KEYS = [
|
|
|
317
394
|
"billing.ui.wizard.review.subscribe_now",
|
|
318
395
|
"billing.ui.wizard.review.trial_upgrade",
|
|
319
396
|
"billing.ui.wizard.review.trial_upgrade_description",
|
|
397
|
+
"billing.ui.wizard.review.trial_upgrade_description_discounted",
|
|
398
|
+
"billing.ui.wizard.review.use_another_card",
|
|
320
399
|
"billing.ui.wizard.steps.select_plan",
|
|
321
400
|
"billing.ui.wizard.subscribe_description",
|
|
322
401
|
] as const;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { RoundPageContainer } from "../../../../../components";
|
|
5
|
+
import { Modules } from "../../../../../core";
|
|
6
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
7
|
+
import { CouponDetails } from "../details/CouponDetails";
|
|
8
|
+
import { CouponCodesList } from "../lists/CouponCodesList";
|
|
9
|
+
|
|
10
|
+
export function CouponContainer() {
|
|
11
|
+
const t = useTranslations();
|
|
12
|
+
const { coupon } = useCouponContext();
|
|
13
|
+
|
|
14
|
+
if (!coupon) return null;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<RoundPageContainer
|
|
18
|
+
module={Modules.StripeCoupon}
|
|
19
|
+
id={coupon.id}
|
|
20
|
+
tabs={[
|
|
21
|
+
{
|
|
22
|
+
sectionKey: "details",
|
|
23
|
+
label: t("billing.admin.products.tabs.details"),
|
|
24
|
+
content: <CouponDetails />,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
sectionKey: "codes",
|
|
28
|
+
label: t("billing.admin.coupons.codes.title"),
|
|
29
|
+
// No `fillHeight` and no `fullWidth` on the list: a ContentListTable in
|
|
30
|
+
// a tab keeps its own bordered card and flows with the panel, exactly
|
|
31
|
+
// as PricesList does inside ProductContainer. The codes list reads the
|
|
32
|
+
// page's own CouponProvider, so no nested provider is needed.
|
|
33
|
+
content: <CouponCodesList couponId={coupon.id} />,
|
|
34
|
+
},
|
|
35
|
+
]}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CouponContainer";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { DetailField, SectionHeader } from "../../../../../components";
|
|
5
|
+
import { Badge } from "../../../../../shadcnui";
|
|
6
|
+
import { formatDate, useBillingFormatOptions } from "../../../components/utils";
|
|
7
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
8
|
+
import { formatCouponDiscount } from "../../utils/format-coupon-discount";
|
|
9
|
+
|
|
10
|
+
export function CouponDetails() {
|
|
11
|
+
const t = useTranslations();
|
|
12
|
+
const fmt = useBillingFormatOptions();
|
|
13
|
+
const { coupon } = useCouponContext();
|
|
14
|
+
|
|
15
|
+
if (!coupon) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
19
|
+
<SectionHeader>{t("billing.admin.products.tabs.details")}</SectionHeader>
|
|
20
|
+
{/* Typography role 10 (helper text). */}
|
|
21
|
+
<p className="text-muted-foreground text-xs/relaxed">{t("billing.admin.coupons.fixed_notice")}</p>
|
|
22
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
23
|
+
<DetailField label={t("billing.admin.coupons.fields.name")} value={coupon.name} />
|
|
24
|
+
<DetailField
|
|
25
|
+
label={t("billing.admin.coupons.fields.status")}
|
|
26
|
+
value={
|
|
27
|
+
coupon.valid ? (
|
|
28
|
+
<Badge variant="softGreen">{t("billing.admin.coupons.status.active")}</Badge>
|
|
29
|
+
) : (
|
|
30
|
+
<Badge variant="softGray">{t("billing.admin.coupons.status.ended")}</Badge>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
/>
|
|
34
|
+
<DetailField label={t("billing.admin.coupons.fields.discount")} value={formatCouponDiscount(coupon, t, fmt)} />
|
|
35
|
+
<DetailField
|
|
36
|
+
label={t("billing.admin.coupons.fields.places")}
|
|
37
|
+
value={
|
|
38
|
+
<span className="tabular-nums">
|
|
39
|
+
{coupon.maxRedemptions !== undefined
|
|
40
|
+
? t("billing.admin.coupons.places.limited", {
|
|
41
|
+
used: String(coupon.timesRedeemed),
|
|
42
|
+
limit: String(coupon.maxRedemptions),
|
|
43
|
+
})
|
|
44
|
+
: t("billing.admin.coupons.places.unlimited", { used: String(coupon.timesRedeemed) })}
|
|
45
|
+
</span>
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
<DetailField
|
|
49
|
+
label={t("billing.admin.coupons.fields.products")}
|
|
50
|
+
value={
|
|
51
|
+
coupon.stripeProductIds.length > 0 ? (
|
|
52
|
+
<span className="tabular-nums">{coupon.stripeProductIds.join(", ")}</span>
|
|
53
|
+
) : (
|
|
54
|
+
t("billing.admin.coupons.all_products")
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
/>
|
|
58
|
+
<DetailField
|
|
59
|
+
label={t("billing.admin.coupons.fields.redeemBy")}
|
|
60
|
+
value={coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : "—"}
|
|
61
|
+
/>
|
|
62
|
+
<DetailField
|
|
63
|
+
label={t("billing.admin.coupons.fields.stripeId")}
|
|
64
|
+
value={<span className="tabular-nums">{coupon.stripeCouponId}</span>}
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CouponDetails";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ReactNode, useCallback, useMemo } from "react";
|
|
6
|
+
import { useForm } from "react-hook-form";
|
|
7
|
+
import { v4 } from "uuid";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { EditorSheet } from "../../../../../components";
|
|
10
|
+
import { Modules } from "../../../../../core";
|
|
11
|
+
import { useI18nRouter } from "../../../../../i18n";
|
|
12
|
+
import { useCouponContext } from "../../../contexts/CouponContext";
|
|
13
|
+
import { CouponCodeFields, couponCodeDefaults, couponCodeSchema, toCouponCodeInput } from "./CouponCodeFields";
|
|
14
|
+
|
|
15
|
+
export type CouponCodeEditorProps = {
|
|
16
|
+
couponId: string;
|
|
17
|
+
onSuccess?: () => void | Promise<void>;
|
|
18
|
+
trigger?: ReactNode;
|
|
19
|
+
forceShow?: boolean;
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
dialogOpen?: boolean;
|
|
22
|
+
onDialogOpenChange?: (open: boolean) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create-only: Stripe allows changing a promotion code's `active` flag and
|
|
27
|
+
* little else, so a code is never edited here. "Apply automatically" and
|
|
28
|
+
* deactivate / reactivate live on the row (CouponCodeActions).
|
|
29
|
+
*/
|
|
30
|
+
function CouponCodeEditorInternal({
|
|
31
|
+
couponId,
|
|
32
|
+
onSuccess,
|
|
33
|
+
trigger,
|
|
34
|
+
forceShow,
|
|
35
|
+
onClose,
|
|
36
|
+
dialogOpen,
|
|
37
|
+
onDialogOpenChange,
|
|
38
|
+
}: CouponCodeEditorProps) {
|
|
39
|
+
const t = useTranslations();
|
|
40
|
+
const router = useI18nRouter();
|
|
41
|
+
const { createCode } = useCouponContext();
|
|
42
|
+
|
|
43
|
+
const formSchema = useMemo(() => z.object({ id: z.uuidv4() }).and(couponCodeSchema(t)), [t]);
|
|
44
|
+
|
|
45
|
+
const getDefaultValues = useCallback(() => ({ id: v4(), ...couponCodeDefaults() }), []);
|
|
46
|
+
|
|
47
|
+
const form = useForm<any>({
|
|
48
|
+
resolver: zodResolver(formSchema as any) as any,
|
|
49
|
+
defaultValues: getDefaultValues(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<EditorSheet
|
|
54
|
+
form={form}
|
|
55
|
+
entityType={t("billing.admin.coupons.codes.entity")}
|
|
56
|
+
isEdit={false}
|
|
57
|
+
module={Modules.StripeCouponCode}
|
|
58
|
+
size="md"
|
|
59
|
+
// A code has no page of its own: without onSuccess EditorSheet would
|
|
60
|
+
// navigate to `/administration/coupons/<codeId>`, which does not exist.
|
|
61
|
+
// The context's createCode already reloads the coupon and the codes list.
|
|
62
|
+
onSuccess={onSuccess ?? (() => {})}
|
|
63
|
+
onNavigate={(url) => router.push(url)}
|
|
64
|
+
onSubmit={async (values: any) => await createCode({ id: values.id, couponId, ...toCouponCodeInput(values) })}
|
|
65
|
+
onReset={getDefaultValues}
|
|
66
|
+
trigger={trigger}
|
|
67
|
+
forceShow={forceShow}
|
|
68
|
+
onClose={onClose}
|
|
69
|
+
dialogOpen={dialogOpen}
|
|
70
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
71
|
+
>
|
|
72
|
+
<CouponCodeFields form={form} />
|
|
73
|
+
</EditorSheet>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default function CouponCodeEditor(props: CouponCodeEditorProps) {
|
|
78
|
+
return <CouponCodeEditorInternal {...props} />;
|
|
79
|
+
}
|