@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.
Files changed (152) hide show
  1. package/dist/{BlockNoteEditor-GVQCKRMU.mjs → BlockNoteEditor-R3WXUFHX.mjs} +4 -4
  2. package/dist/{BlockNoteEditor-THBJH2UR.js → BlockNoteEditor-ZHXXFWUJ.js} +19 -19
  3. package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-ZHXXFWUJ.js.map} +1 -1
  4. package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-BVVN6D3T.mjs} +2 -2
  5. package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-SD3ZFIXD.js} +3 -3
  6. package/dist/{BlockNoteViewer-VZOVKTFO.js.map → BlockNoteViewer-SD3ZFIXD.js.map} +1 -1
  7. package/dist/billing/index.d.mts +197 -46
  8. package/dist/billing/index.d.ts +197 -46
  9. package/dist/billing/index.js +1166 -589
  10. package/dist/billing/index.js.map +1 -1
  11. package/dist/billing/index.mjs +803 -226
  12. package/dist/billing/index.mjs.map +1 -1
  13. package/dist/{chunk-WPYT4UKJ.js → chunk-2TXE57LE.js} +3 -3
  14. package/dist/{chunk-WPYT4UKJ.js.map → chunk-2TXE57LE.js.map} +1 -1
  15. package/dist/{chunk-G6UE6MEP.js → chunk-5TH2DSAH.js} +901 -204
  16. package/dist/chunk-5TH2DSAH.js.map +1 -0
  17. package/dist/{chunk-X5WIUUNG.mjs → chunk-CFB6FHIY.mjs} +3342 -2377
  18. package/dist/chunk-CFB6FHIY.mjs.map +1 -0
  19. package/dist/{chunk-VC5A3AUZ.mjs → chunk-FK26NX3S.mjs} +701 -4
  20. package/dist/chunk-FK26NX3S.mjs.map +1 -0
  21. package/dist/{chunk-YDYVJM4I.js → chunk-IJME5SAM.js} +4 -4
  22. package/dist/{chunk-YDYVJM4I.js.map → chunk-IJME5SAM.js.map} +1 -1
  23. package/dist/{chunk-W7F5BDSV.mjs → chunk-OKCV4GPJ.mjs} +2 -2
  24. package/dist/{chunk-3CGROPF3.mjs → chunk-RGQYFSJF.mjs} +2 -2
  25. package/dist/{chunk-6SQYXUVP.mjs → chunk-WCHLCOZG.mjs} +2 -2
  26. package/dist/{chunk-S4YJ5WCW.js → chunk-XOSC3FFP.js} +8 -8
  27. package/dist/{chunk-S4YJ5WCW.js.map → chunk-XOSC3FFP.js.map} +1 -1
  28. package/dist/{chunk-TY5FYP3G.js → chunk-ZCOPOO7T.js} +2024 -1059
  29. package/dist/chunk-ZCOPOO7T.js.map +1 -0
  30. package/dist/client/index.js +4 -4
  31. package/dist/client/index.mjs +3 -3
  32. package/dist/components/index.js +4 -4
  33. package/dist/components/index.mjs +3 -3
  34. package/dist/contexts/index.js +4 -4
  35. package/dist/contexts/index.mjs +3 -3
  36. package/dist/core/index.d.mts +248 -3
  37. package/dist/core/index.d.ts +248 -3
  38. package/dist/core/index.js +42 -2
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/index.mjs +41 -1
  41. package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → currency-DT_dveBx.d.mts} +137 -1
  42. package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → currency-pK9eRYRC.d.ts} +137 -1
  43. package/dist/email-automations/index.js +92 -92
  44. package/dist/email-automations/index.mjs +3 -3
  45. package/dist/features/analytics/index.js +121 -121
  46. package/dist/features/analytics/index.mjs +4 -4
  47. package/dist/features/analytics/server.js +4 -4
  48. package/dist/features/analytics/server.mjs +2 -2
  49. package/dist/features/blog/index.js +150 -150
  50. package/dist/features/blog/index.mjs +3 -3
  51. package/dist/features/blog/server.js +16 -16
  52. package/dist/features/blog/server.mjs +1 -1
  53. package/dist/features/help/index.js +37 -37
  54. package/dist/features/help/index.mjs +3 -3
  55. package/dist/features/tokenusage/index.js +124 -124
  56. package/dist/features/tokenusage/index.mjs +4 -4
  57. package/dist/index.d.mts +2 -2
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +43 -3
  60. package/dist/index.js.map +1 -1
  61. package/dist/index.mjs +42 -2
  62. package/dist/server/index.js +3 -3
  63. package/dist/server/index.mjs +2 -2
  64. package/messages/en.json +125 -3
  65. package/messages/it.json +125 -3
  66. package/package.json +1 -1
  67. package/scripts/validate-messages.mjs +1 -0
  68. package/src/billing/index.ts +1 -0
  69. package/src/client/index.ts +4 -0
  70. package/src/core/index.ts +4 -0
  71. package/src/core/registry/ModuleRegistry.ts +3 -0
  72. package/src/features/billing/components/cards/InvoicesSummaryCard.tsx +3 -2
  73. package/src/features/billing/components/cards/SubscriptionSummaryCard.tsx +4 -3
  74. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +22 -5
  75. package/src/features/billing/components/utils/__tests__/discount.spec.ts +59 -0
  76. package/src/features/billing/components/utils/__tests__/url.spec.ts +32 -0
  77. package/src/features/billing/components/utils/discount.ts +38 -0
  78. package/src/features/billing/components/utils/index.ts +2 -0
  79. package/src/features/billing/components/utils/url.ts +12 -0
  80. package/src/features/billing/contexts/CouponContext.tsx +209 -0
  81. package/src/features/billing/contexts/index.ts +1 -0
  82. package/src/features/billing/i18n-keys.ts +79 -0
  83. package/src/features/billing/stripe-coupon/components/containers/CouponContainer.tsx +38 -0
  84. package/src/features/billing/stripe-coupon/components/containers/index.ts +1 -0
  85. package/src/features/billing/stripe-coupon/components/details/CouponDetails.tsx +72 -0
  86. package/src/features/billing/stripe-coupon/components/details/index.ts +1 -0
  87. package/src/features/billing/stripe-coupon/components/forms/CouponCodeEditor.tsx +80 -0
  88. package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +214 -0
  89. package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +425 -0
  90. package/src/features/billing/stripe-coupon/components/forms/CouponRefresher.tsx +45 -0
  91. package/src/features/billing/stripe-coupon/components/forms/index.ts +6 -0
  92. package/src/features/billing/stripe-coupon/components/index.ts +5 -0
  93. package/src/features/billing/stripe-coupon/components/lists/CouponCodesList.tsx +61 -0
  94. package/src/features/billing/stripe-coupon/components/lists/CouponsList.tsx +67 -0
  95. package/src/features/billing/stripe-coupon/components/lists/index.ts +2 -0
  96. package/src/features/billing/stripe-coupon/components/widgets/CouponCodeActions.tsx +81 -0
  97. package/src/features/billing/stripe-coupon/components/widgets/index.ts +1 -0
  98. package/src/features/billing/stripe-coupon/data/__tests__/stripe-coupon-models.spec.ts +114 -0
  99. package/src/features/billing/stripe-coupon/data/index.ts +10 -0
  100. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.fields.ts +8 -0
  101. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.interface.ts +44 -0
  102. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.service.ts +92 -0
  103. package/src/features/billing/stripe-coupon/data/stripe-coupon-code.ts +132 -0
  104. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.interface.ts +17 -0
  105. package/src/features/billing/stripe-coupon/data/stripe-coupon-offer.ts +70 -0
  106. package/src/features/billing/stripe-coupon/data/stripe-coupon.fields.ts +7 -0
  107. package/src/features/billing/stripe-coupon/data/stripe-coupon.interface.ts +55 -0
  108. package/src/features/billing/stripe-coupon/data/stripe-coupon.service.ts +93 -0
  109. package/src/features/billing/stripe-coupon/data/stripe-coupon.ts +167 -0
  110. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +161 -0
  111. package/src/features/billing/stripe-coupon/hooks/useStripeCouponTableStructure.tsx +113 -0
  112. package/src/features/billing/stripe-coupon/index.ts +5 -0
  113. package/src/features/billing/stripe-coupon/stripe-coupon-code.module.ts +15 -0
  114. package/src/features/billing/stripe-coupon/stripe-coupon-offer.module.ts +10 -0
  115. package/src/features/billing/stripe-coupon/stripe-coupon.module.ts +25 -0
  116. package/src/features/billing/stripe-coupon/utils/__tests__/coupon-places.spec.ts +19 -0
  117. package/src/features/billing/stripe-coupon/utils/__tests__/to-promotion-code-result.spec.ts +41 -0
  118. package/src/features/billing/stripe-coupon/utils/coupon-places.ts +23 -0
  119. package/src/features/billing/stripe-coupon/utils/format-coupon-discount.ts +36 -0
  120. package/src/features/billing/stripe-coupon/utils/index.ts +3 -0
  121. package/src/features/billing/stripe-coupon/utils/to-promotion-code-result.ts +23 -0
  122. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +24 -8
  123. package/src/features/billing/stripe-product/components/containers/ProductsListContainer.tsx +21 -6
  124. package/src/features/billing/stripe-promotion-code/components/PromoCodeInput.tsx +60 -19
  125. package/src/features/billing/stripe-promotion-code/data/stripe-promotion-code.interface.ts +2 -0
  126. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -5
  127. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +21 -2
  128. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +1 -1
  129. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +1 -1
  130. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +60 -49
  131. package/src/features/billing/stripe-subscription/components/wizards/__tests__/review-card.spec.ts +34 -0
  132. package/src/features/billing/stripe-subscription/components/wizards/review-card.ts +24 -0
  133. package/src/features/billing/stripe-subscription/data/__tests__/stripe-subscription.discount.spec.ts +67 -0
  134. package/src/features/billing/stripe-subscription/data/stripe-subscription.interface.ts +14 -0
  135. package/src/features/billing/stripe-subscription/data/stripe-subscription.service.ts +39 -0
  136. package/src/features/billing/stripe-subscription/data/stripe-subscription.ts +21 -0
  137. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-payment.spec.ts +48 -0
  138. package/src/features/billing/stripe-subscription/hooks/__tests__/wizard-promotion.spec.ts +132 -0
  139. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +138 -12
  140. package/src/features/billing/stripe-subscription/hooks/wizard-payment.ts +30 -0
  141. package/src/features/billing/stripe-subscription/hooks/wizard-promotion.ts +61 -0
  142. package/src/features/user/contexts/CurrentUserContext.tsx +25 -0
  143. package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +121 -2
  144. package/dist/chunk-G6UE6MEP.js.map +0 -1
  145. package/dist/chunk-TY5FYP3G.js.map +0 -1
  146. package/dist/chunk-VC5A3AUZ.mjs.map +0 -1
  147. package/dist/chunk-X5WIUUNG.mjs.map +0 -1
  148. /package/dist/{BlockNoteEditor-GVQCKRMU.mjs.map → BlockNoteEditor-R3WXUFHX.mjs.map} +0 -0
  149. /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-BVVN6D3T.mjs.map} +0 -0
  150. /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-OKCV4GPJ.mjs.map} +0 -0
  151. /package/dist/{chunk-3CGROPF3.mjs.map → chunk-RGQYFSJF.mjs.map} +0 -0
  152. /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;
@@ -1,2 +1,3 @@
1
+ export * from "./CouponContext";
1
2
  export * from "./PriceContext";
2
3
  export * from "./ProductContext";
@@ -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
+ }