@carlonicora/nextjs-jsonapi 5.5.0 → 5.7.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-GVQCKRMU.mjs → BlockNoteEditor-R3WXUFHX.mjs} +4 -4
- package/dist/{BlockNoteEditor-THBJH2UR.js → BlockNoteEditor-ZHXXFWUJ.js} +19 -19
- package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-ZHXXFWUJ.js.map} +1 -1
- package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-BVVN6D3T.mjs} +2 -2
- package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-SD3ZFIXD.js} +3 -3
- package/dist/{BlockNoteViewer-VZOVKTFO.js.map → BlockNoteViewer-SD3ZFIXD.js.map} +1 -1
- package/dist/billing/index.d.mts +197 -46
- package/dist/billing/index.d.ts +197 -46
- package/dist/billing/index.js +1166 -589
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +803 -226
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-WPYT4UKJ.js → chunk-2TXE57LE.js} +3 -3
- package/dist/{chunk-WPYT4UKJ.js.map → chunk-2TXE57LE.js.map} +1 -1
- package/dist/{chunk-G6UE6MEP.js → chunk-5TH2DSAH.js} +901 -204
- package/dist/chunk-5TH2DSAH.js.map +1 -0
- package/dist/{chunk-X5WIUUNG.mjs → chunk-CFB6FHIY.mjs} +3342 -2377
- package/dist/chunk-CFB6FHIY.mjs.map +1 -0
- package/dist/{chunk-VC5A3AUZ.mjs → chunk-FK26NX3S.mjs} +701 -4
- package/dist/chunk-FK26NX3S.mjs.map +1 -0
- package/dist/{chunk-YDYVJM4I.js → chunk-IJME5SAM.js} +4 -4
- package/dist/{chunk-YDYVJM4I.js.map → chunk-IJME5SAM.js.map} +1 -1
- package/dist/{chunk-W7F5BDSV.mjs → chunk-OKCV4GPJ.mjs} +2 -2
- package/dist/{chunk-3CGROPF3.mjs → chunk-RGQYFSJF.mjs} +2 -2
- package/dist/{chunk-6SQYXUVP.mjs → chunk-WCHLCOZG.mjs} +2 -2
- package/dist/{chunk-S4YJ5WCW.js → chunk-XOSC3FFP.js} +8 -8
- package/dist/{chunk-S4YJ5WCW.js.map → chunk-XOSC3FFP.js.map} +1 -1
- package/dist/{chunk-TY5FYP3G.js → chunk-ZCOPOO7T.js} +2024 -1059
- package/dist/chunk-ZCOPOO7T.js.map +1 -0
- 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 +248 -3
- package/dist/core/index.d.ts +248 -3
- package/dist/core/index.js +42 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +41 -1
- package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → currency-DT_dveBx.d.mts} +137 -1
- package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → currency-pK9eRYRC.d.ts} +137 -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/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -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 +4 -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 +209 -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 +72 -0
- package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +80 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +214 -0
- package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +425 -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 +81 -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 +55 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
- package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +167 -0
- package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +161 -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 +15 -0
- package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
- package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +25 -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 +61 -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-R3WXUFHX.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-BVVN6D3T.mjs.map} +0 -0
- /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-OKCV4GPJ.mjs.map} +0 -0
- /package/dist/{chunk-3CGROPF3.mjs.map → chunk-RGQYFSJF.mjs.map} +0 -0
- /package/dist/{chunk-6SQYXUVP.mjs.map → chunk-WCHLCOZG.mjs.map} +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
if (hasRole(getRoleId().Administrator)) {
|
|
193
|
+
response.functions = [
|
|
194
|
+
<CouponRefresher key="couponRefresher" coupon={coupon} />,
|
|
195
|
+
<CouponEditor key="couponEditor" coupon={coupon} propagateChanges={setCoupon} />,
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return response;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<SharedProvider value={{ breadcrumbs: breadcrumb(), title: title() }}>
|
|
204
|
+
<CouponContext.Provider value={value}>{children}</CouponContext.Provider>
|
|
205
|
+
</SharedProvider>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
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,72 @@
|
|
|
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
|
|
35
|
+
label={t("billing.admin.coupons.fields.discount")}
|
|
36
|
+
value={<span className="tabular-nums">{formatCouponDiscount(coupon, t, fmt)}</span>}
|
|
37
|
+
/>
|
|
38
|
+
<DetailField
|
|
39
|
+
label={t("billing.admin.coupons.fields.places")}
|
|
40
|
+
value={
|
|
41
|
+
<span className="tabular-nums">
|
|
42
|
+
{coupon.maxRedemptions !== undefined
|
|
43
|
+
? t("billing.admin.coupons.places.limited", {
|
|
44
|
+
used: String(coupon.timesRedeemed),
|
|
45
|
+
limit: String(coupon.maxRedemptions),
|
|
46
|
+
})
|
|
47
|
+
: t("billing.admin.coupons.places.unlimited", { used: String(coupon.timesRedeemed) })}
|
|
48
|
+
</span>
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
<DetailField
|
|
52
|
+
label={t("billing.admin.coupons.fields.products")}
|
|
53
|
+
value={
|
|
54
|
+
coupon.stripeProductIds.length > 0 ? (
|
|
55
|
+
<span className="tabular-nums">{coupon.stripeProductIds.join(", ")}</span>
|
|
56
|
+
) : (
|
|
57
|
+
t("billing.admin.coupons.all_products")
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
<DetailField
|
|
62
|
+
label={t("billing.admin.coupons.fields.redeemBy")}
|
|
63
|
+
value={<span className="tabular-nums">{coupon.redeemBy ? formatDate(coupon.redeemBy, fmt) : "—"}</span>}
|
|
64
|
+
/>
|
|
65
|
+
<DetailField
|
|
66
|
+
label={t("billing.admin.coupons.fields.stripeId")}
|
|
67
|
+
value={<span className="tabular-nums">{coupon.stripeCouponId}</span>}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./CouponDetails";
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
type CouponCodeEditorValues = z.infer<typeof formSchema>;
|
|
45
|
+
|
|
46
|
+
const getDefaultValues = useCallback((): CouponCodeEditorValues => ({ id: v4(), ...couponCodeDefaults() }), []);
|
|
47
|
+
|
|
48
|
+
const form = useForm<CouponCodeEditorValues>({
|
|
49
|
+
resolver: zodResolver(formSchema),
|
|
50
|
+
defaultValues: getDefaultValues(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<EditorSheet
|
|
55
|
+
form={form}
|
|
56
|
+
entityType={t("billing.admin.coupons.codes.entity")}
|
|
57
|
+
isEdit={false}
|
|
58
|
+
module={Modules.StripeCouponCode}
|
|
59
|
+
size="md"
|
|
60
|
+
// A code has no page of its own: without onSuccess EditorSheet would
|
|
61
|
+
// navigate to `<module pageUrl>/<codeId>`, which does not exist.
|
|
62
|
+
// The context's createCode already reloads the coupon and the codes list.
|
|
63
|
+
onSuccess={onSuccess ?? (() => {})}
|
|
64
|
+
onNavigate={(url) => router.push(url)}
|
|
65
|
+
onSubmit={async (values) => await createCode({ id: values.id, couponId, ...toCouponCodeInput(values) })}
|
|
66
|
+
onReset={getDefaultValues}
|
|
67
|
+
trigger={trigger}
|
|
68
|
+
forceShow={forceShow}
|
|
69
|
+
onClose={onClose}
|
|
70
|
+
dialogOpen={dialogOpen}
|
|
71
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
72
|
+
>
|
|
73
|
+
<CouponCodeFields form={form} />
|
|
74
|
+
</EditorSheet>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function CouponCodeEditor(props: CouponCodeEditorProps) {
|
|
79
|
+
return <CouponCodeEditorInternal {...props} />;
|
|
80
|
+
}
|