@carlonicora/nextjs-jsonapi 6.2.0 → 6.4.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/{AssistantMessageInterface-S7oyERTT.d.mts → AssistantMessageInterface-0ZQ1EJKO.d.mts} +1 -1
- package/dist/{AssistantMessageInterface-DHJJO_Z_.d.ts → AssistantMessageInterface-CA-hZ5Wj.d.ts} +1 -1
- package/dist/{BlockNoteEditor-Q757ZLCO.js → BlockNoteEditor-CT4XUXPC.js} +19 -19
- package/dist/{BlockNoteEditor-Q757ZLCO.js.map → BlockNoteEditor-CT4XUXPC.js.map} +1 -1
- package/dist/{BlockNoteEditor-D6VMXRZH.mjs → BlockNoteEditor-M6UQRVRK.mjs} +4 -4
- package/dist/{BlockNoteViewer-KG4CIJC5.js → BlockNoteViewer-MFLWF3Y2.js} +3 -3
- package/dist/{BlockNoteViewer-KG4CIJC5.js.map → BlockNoteViewer-MFLWF3Y2.js.map} +1 -1
- package/dist/{BlockNoteViewer-GI5LVQHZ.mjs → BlockNoteViewer-XZJ5Y7WN.mjs} +2 -2
- package/dist/{BlogPostInterface-BB_4wcDR.d.mts → BlogPostInterface-BN2mXc46.d.mts} +1 -1
- package/dist/{BlogPostInterface-bWAT1rfR.d.ts → BlogPostInterface-CTGfsR1I.d.ts} +1 -1
- package/dist/{auth.interface-CSzJ1oP1.d.ts → auth.interface-D5606g19.d.ts} +1 -1
- package/dist/{auth.interface-DD4uQv2K.d.mts → auth.interface-tsOuc0Fi.d.mts} +1 -1
- package/dist/billing/index.d.mts +3 -3
- package/dist/billing/index.d.ts +3 -3
- package/dist/billing/index.js +501 -493
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +13 -5
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-QKDPCZ3G.mjs → chunk-3FD6Y734.mjs} +73 -15
- package/dist/chunk-3FD6Y734.mjs.map +1 -0
- package/dist/{chunk-3P3IYUWQ.js → chunk-DSGP6GC3.js} +8 -8
- package/dist/{chunk-3P3IYUWQ.js.map → chunk-DSGP6GC3.js.map} +1 -1
- package/dist/{chunk-MHBE4KQR.mjs → chunk-DYOSXIEL.mjs} +26 -21
- package/dist/chunk-DYOSXIEL.mjs.map +1 -0
- package/dist/{chunk-S332ZCIE.js → chunk-ELQHE2DK.js} +27 -22
- package/dist/chunk-ELQHE2DK.js.map +1 -0
- package/dist/{chunk-HLW3SUMD.mjs → chunk-O2TEUH5S.mjs} +2 -2
- package/dist/{chunk-UEWDYVLW.js → chunk-QJIDORHM.js} +4 -4
- package/dist/{chunk-UEWDYVLW.js.map → chunk-QJIDORHM.js.map} +1 -1
- package/dist/{chunk-CUNSPLOD.mjs → chunk-RUJCWOJE.mjs} +2 -2
- package/dist/{chunk-6PW3562E.mjs → chunk-TIT4CJYQ.mjs} +1233 -1181
- package/dist/chunk-TIT4CJYQ.mjs.map +1 -0
- package/dist/{chunk-GSHMGL6N.js → chunk-XZCCIKX3.js} +151 -93
- package/dist/chunk-XZCCIKX3.js.map +1 -0
- package/dist/{chunk-XQEYBESH.js → chunk-Y7JIQNM6.js} +922 -870
- package/dist/chunk-Y7JIQNM6.js.map +1 -0
- package/dist/client/index.d.mts +4 -4
- package/dist/client/index.d.ts +4 -4
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +22 -6
- package/dist/components/index.d.ts +22 -6
- package/dist/components/index.js +10 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -4
- package/dist/contexts/index.d.mts +5 -5
- package/dist/contexts/index.d.ts +5 -5
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +23 -13
- package/dist/core/index.d.ts +23 -13
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/{currency-DrEVd_Xs.d.mts → currency-BhuEoBOn.d.mts} +2 -115
- package/dist/{currency-8ErEC39n.d.ts → currency-CBgZkS0u.d.ts} +2 -115
- package/dist/email-automations/index.js +92 -92
- package/dist/email-automations/index.mjs +3 -3
- package/dist/features/analytics/index.d.mts +2 -2
- package/dist/features/analytics/index.d.ts +2 -2
- 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.d.mts +4 -4
- package/dist/features/blog/index.d.ts +4 -4
- package/dist/features/blog/index.js +150 -150
- package/dist/features/blog/index.mjs +3 -3
- package/dist/features/blog/server.d.mts +4 -4
- package/dist/features/blog/server.d.ts +4 -4
- 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.d.mts +123 -1
- package/dist/features/tokenusage/index.d.ts +123 -1
- package/dist/features/tokenusage/index.js +130 -127
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +7 -4
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +8 -8
- package/dist/index.d.ts +8 -8
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/{notification.interface-BB6GPpZY.d.mts → notification.interface-CF5h90U2.d.mts} +1 -1
- package/dist/{notification.interface-CmAGrijJ.d.ts → notification.interface-DQReXAD7.d.ts} +1 -1
- package/dist/{s3.service-Bz_K-tjz.d.ts → s3.service-BTCpmp5g.d.ts} +11 -4
- package/dist/{s3.service-D5zNCEW3.d.mts → s3.service-CzFBDEuQ.d.mts} +11 -4
- package/dist/server/index.d.mts +5 -5
- package/dist/server/index.d.ts +5 -5
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +2 -2
- package/dist/stripe-price.interface-D6mKjAdr.d.mts +132 -0
- package/dist/stripe-price.interface-DGqcklFJ.d.ts +132 -0
- package/dist/{useSocket-B0H9pmmH.d.ts → useSocket-BM3q9A0C.d.ts} +1 -1
- package/dist/{useSocket-B5RQb6v2.d.mts → useSocket-CwSoq6VS.d.mts} +1 -1
- package/dist/{user.interface-CIWGlbqM.d.mts → user.interface-BYnGJ7ss.d.mts} +37 -2
- package/dist/{user.interface-C0ZdsBZj.d.ts → user.interface-BqPjVyJI.d.ts} +37 -2
- package/messages/en.json +8 -0
- package/messages/it.json +8 -0
- package/package.json +1 -1
- package/src/components/containers/RoundPageContainer.tsx +7 -2
- package/src/components/index.ts +5 -0
- package/src/features/billing/components/containers/BillingDashboardContainer.tsx +20 -3
- package/src/features/billing/components/widgets/CreditWarningBanner.tsx +93 -0
- package/src/features/billing/data/credit-warning.spec.ts +96 -0
- package/src/features/billing/data/credit-warning.ts +57 -0
- package/src/features/billing/data/index.ts +3 -0
- package/src/features/billing/i18n-keys.ts +6 -0
- package/src/features/company/data/company.interface.ts +11 -0
- package/src/features/company/data/company.service.ts +17 -0
- package/src/features/company/data/company.spec.ts +29 -0
- package/src/features/company/data/company.ts +31 -0
- package/src/features/tokenusage/index.ts +5 -0
- package/dist/chunk-6PW3562E.mjs.map +0 -1
- package/dist/chunk-GSHMGL6N.js.map +0 -1
- package/dist/chunk-MHBE4KQR.mjs.map +0 -1
- package/dist/chunk-QKDPCZ3G.mjs.map +0 -1
- package/dist/chunk-S332ZCIE.js.map +0 -1
- package/dist/chunk-XQEYBESH.js.map +0 -1
- package/dist/feature.interface-BO25VLlx.d.ts +0 -19
- package/dist/feature.interface-CXb1-vNq.d.mts +0 -19
- /package/dist/{BlockNoteEditor-D6VMXRZH.mjs.map → BlockNoteEditor-M6UQRVRK.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-GI5LVQHZ.mjs.map → BlockNoteViewer-XZJ5Y7WN.mjs.map} +0 -0
- /package/dist/{chunk-HLW3SUMD.mjs.map → chunk-O2TEUH5S.mjs.map} +0 -0
- /package/dist/{chunk-CUNSPLOD.mjs.map → chunk-RUJCWOJE.mjs.map} +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { A as ApiDataInterface } from './ApiDataInterface-BcZeXy5X.js';
|
|
2
|
+
|
|
3
|
+
interface ModuleInterface extends ApiDataInterface {
|
|
4
|
+
get name(): string;
|
|
5
|
+
get permissions(): {
|
|
6
|
+
create: boolean | string;
|
|
7
|
+
read: boolean | string;
|
|
8
|
+
update: boolean | string;
|
|
9
|
+
delete: boolean | string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface FeatureInterface extends ApiDataInterface {
|
|
14
|
+
get name(): string;
|
|
15
|
+
get isCore(): boolean;
|
|
16
|
+
get modules(): ModuleInterface[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare enum SubscriptionStatus {
|
|
20
|
+
ACTIVE = "active",
|
|
21
|
+
PAST_DUE = "past_due",
|
|
22
|
+
UNPAID = "unpaid",
|
|
23
|
+
CANCELED = "canceled",
|
|
24
|
+
INCOMPLETE = "incomplete",
|
|
25
|
+
INCOMPLETE_EXPIRED = "incomplete_expired",
|
|
26
|
+
TRIALING = "trialing",
|
|
27
|
+
PAUSED = "paused"
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The discount Stripe applies to the subscription (read-only, synced from Stripe).
|
|
31
|
+
* Exactly one of percentOff / amountOff is set.
|
|
32
|
+
*/
|
|
33
|
+
type StripeSubscriptionDiscount = {
|
|
34
|
+
percentOff?: number;
|
|
35
|
+
amountOff?: number;
|
|
36
|
+
currency?: string;
|
|
37
|
+
duration: "forever" | "once" | "repeating";
|
|
38
|
+
durationInMonths?: number;
|
|
39
|
+
end?: Date;
|
|
40
|
+
};
|
|
41
|
+
interface StripeSubscriptionInterface extends ApiDataInterface {
|
|
42
|
+
get stripeSubscriptionId(): string;
|
|
43
|
+
get status(): SubscriptionStatus;
|
|
44
|
+
get currentPeriodStart(): Date;
|
|
45
|
+
get currentPeriodEnd(): Date;
|
|
46
|
+
get cancelAtPeriodEnd(): boolean;
|
|
47
|
+
get canceledAt(): Date | undefined;
|
|
48
|
+
get trialStart(): Date | undefined;
|
|
49
|
+
get trialEnd(): Date | undefined;
|
|
50
|
+
get price(): StripePriceInterface | undefined;
|
|
51
|
+
get discount(): StripeSubscriptionDiscount | undefined;
|
|
52
|
+
}
|
|
53
|
+
type StripeSubscriptionInput = {
|
|
54
|
+
id: string;
|
|
55
|
+
priceId?: string;
|
|
56
|
+
newPriceId?: string;
|
|
57
|
+
cancelImmediately?: boolean;
|
|
58
|
+
quantity?: number;
|
|
59
|
+
trialPeriodDays?: number;
|
|
60
|
+
paymentMethodId?: string;
|
|
61
|
+
metadata?: Record<string, any>;
|
|
62
|
+
promotionCode?: string;
|
|
63
|
+
};
|
|
64
|
+
interface StripeSubscriptionCreateMeta {
|
|
65
|
+
clientSecret: string | null;
|
|
66
|
+
paymentIntentId: string | null;
|
|
67
|
+
requiresAction: boolean;
|
|
68
|
+
}
|
|
69
|
+
interface StripeSubscriptionCreateResponse {
|
|
70
|
+
subscription: StripeSubscriptionInterface;
|
|
71
|
+
meta: StripeSubscriptionCreateMeta;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface StripeProductInterface extends ApiDataInterface {
|
|
75
|
+
get stripeProductId(): string;
|
|
76
|
+
get name(): string;
|
|
77
|
+
get description(): string | undefined;
|
|
78
|
+
get active(): boolean;
|
|
79
|
+
get metadata(): Record<string, any> | undefined;
|
|
80
|
+
get stripePrices(): StripePriceInterface[];
|
|
81
|
+
}
|
|
82
|
+
type StripeProductInput = {
|
|
83
|
+
id: string;
|
|
84
|
+
name?: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
active?: boolean;
|
|
87
|
+
metadata?: Record<string, any>;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
interface StripePriceInterface extends ApiDataInterface {
|
|
91
|
+
get stripePriceId(): string;
|
|
92
|
+
get productId(): string;
|
|
93
|
+
get product(): StripeProductInterface | undefined;
|
|
94
|
+
get active(): boolean;
|
|
95
|
+
get currency(): string;
|
|
96
|
+
get unitAmount(): number | undefined;
|
|
97
|
+
get recurring(): PriceRecurring | undefined;
|
|
98
|
+
get priceType(): "one_time" | "recurring";
|
|
99
|
+
get nickname(): string | undefined;
|
|
100
|
+
get lookupKey(): string | undefined;
|
|
101
|
+
get metadata(): Record<string, any> | undefined;
|
|
102
|
+
get description(): string | undefined;
|
|
103
|
+
get features(): string[] | undefined;
|
|
104
|
+
get token(): number | undefined;
|
|
105
|
+
get isTrial(): boolean | undefined;
|
|
106
|
+
get priceFeatures(): FeatureInterface[];
|
|
107
|
+
}
|
|
108
|
+
interface PriceRecurring {
|
|
109
|
+
interval: "day" | "week" | "month" | "year";
|
|
110
|
+
intervalCount: number;
|
|
111
|
+
usageType?: "metered" | "licensed";
|
|
112
|
+
}
|
|
113
|
+
type StripePriceInput = {
|
|
114
|
+
id: string;
|
|
115
|
+
productId?: string;
|
|
116
|
+
currency?: string;
|
|
117
|
+
unitAmount?: number;
|
|
118
|
+
nickname?: string;
|
|
119
|
+
recurring?: {
|
|
120
|
+
interval: "day" | "week" | "month" | "year";
|
|
121
|
+
intervalCount?: number;
|
|
122
|
+
usageType?: "metered" | "licensed";
|
|
123
|
+
};
|
|
124
|
+
metadata?: Record<string, any>;
|
|
125
|
+
description?: string;
|
|
126
|
+
features?: string[];
|
|
127
|
+
token?: number;
|
|
128
|
+
isTrial?: boolean;
|
|
129
|
+
featureIds?: string[];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export { type FeatureInterface as F, type ModuleInterface as M, type PriceRecurring as P, type StripeSubscriptionInterface as S, type StripePriceInterface as a, type StripePriceInput as b, type StripeProductInterface as c, type StripeProductInput as d, SubscriptionStatus as e, type StripeSubscriptionDiscount as f, type StripeSubscriptionInput as g, type StripeSubscriptionCreateMeta as h, type StripeSubscriptionCreateResponse as i };
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { A as ApiDataInterface } from './ApiDataInterface-BcZeXy5X.mjs';
|
|
2
|
-
import { F as FeatureInterface, M as ModuleInterface } from './
|
|
2
|
+
import { S as StripeSubscriptionInterface, F as FeatureInterface, M as ModuleInterface } from './stripe-price.interface-D6mKjAdr.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The offer the server chose when it sent this period's warning.
|
|
6
|
+
* Mirrors the backend `CreditWarningOffer`.
|
|
7
|
+
*/
|
|
8
|
+
type CreditWarningOffer = "subscribe" | "upgrade-or-pack" | "pack";
|
|
9
|
+
/**
|
|
10
|
+
* Child segment of the dismiss route: `POST companies/:companyId/credit-warning/dismiss`.
|
|
11
|
+
* Must match the backend `CREDIT_WARNING_DISMISS_SEGMENT`.
|
|
12
|
+
*/
|
|
13
|
+
declare const CREDIT_WARNING_DISMISS_SEGMENT = "credit-warning/dismiss";
|
|
14
|
+
/**
|
|
15
|
+
* Whether the in-app credit warning should show for this company.
|
|
16
|
+
*
|
|
17
|
+
* True only when the server sent the warning this period and nobody dismissed it,
|
|
18
|
+
* the company has AI and an active subscription, and the credits left are at or
|
|
19
|
+
* below `100 - thresholdPercent` percent of this period's grant. At 0 credits
|
|
20
|
+
* (or below) it is false: `OutOfCreditsBanner` owns that state.
|
|
21
|
+
*/
|
|
22
|
+
declare function isCreditWarningActive(company: CompanyInterface, thresholdPercent: number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The subscription the `?action=change-plan` deep link opens the wizard on: the
|
|
25
|
+
* first active or trialing recurring subscription, in the dashboard's own order.
|
|
26
|
+
* `undefined` means there is none, and the subscribe wizard opens instead.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveChangePlanTarget(subscriptions: StripeSubscriptionInterface[]): StripeSubscriptionInterface | undefined;
|
|
3
29
|
|
|
4
30
|
type CompanyInput = {
|
|
5
31
|
id: string;
|
|
@@ -37,6 +63,15 @@ interface CompanyInterface extends ApiDataInterface {
|
|
|
37
63
|
* defaults an absent attribute to `true`, so the getter always has a value.
|
|
38
64
|
*/
|
|
39
65
|
get aiEnabled(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Credit-warning state, written by the server only (never sent by
|
|
68
|
+
* `createJsonApi()`). All four are absent until the feature is on and the
|
|
69
|
+
* company has received a monthly grant.
|
|
70
|
+
*/
|
|
71
|
+
get creditsGrantedThisPeriod(): number | undefined;
|
|
72
|
+
get creditWarningSentAt(): Date | undefined;
|
|
73
|
+
get creditWarningDismissedAt(): Date | undefined;
|
|
74
|
+
get creditWarningOffer(): CreditWarningOffer | undefined;
|
|
40
75
|
get features(): FeatureInterface[];
|
|
41
76
|
get modules(): ModuleInterface[];
|
|
42
77
|
get legal_address(): string | undefined;
|
|
@@ -99,4 +134,4 @@ interface UserInterface extends ApiDataInterface {
|
|
|
99
134
|
get modules(): ModuleInterface[];
|
|
100
135
|
}
|
|
101
136
|
|
|
102
|
-
export type
|
|
137
|
+
export { type CreditWarningOffer as C, type RoleInput as R, type UserInterface as U, CREDIT_WARNING_DISMISS_SEGMENT as a, type CompanyInput as b, type CompanyInterface as c, type RoleInterface as d, type UserInput as e, isCreditWarningActive as i, resolveChangePlanTarget as r };
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { A as ApiDataInterface } from './ApiDataInterface-BcZeXy5X.js';
|
|
2
|
-
import { F as FeatureInterface, M as ModuleInterface } from './
|
|
2
|
+
import { S as StripeSubscriptionInterface, F as FeatureInterface, M as ModuleInterface } from './stripe-price.interface-DGqcklFJ.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The offer the server chose when it sent this period's warning.
|
|
6
|
+
* Mirrors the backend `CreditWarningOffer`.
|
|
7
|
+
*/
|
|
8
|
+
type CreditWarningOffer = "subscribe" | "upgrade-or-pack" | "pack";
|
|
9
|
+
/**
|
|
10
|
+
* Child segment of the dismiss route: `POST companies/:companyId/credit-warning/dismiss`.
|
|
11
|
+
* Must match the backend `CREDIT_WARNING_DISMISS_SEGMENT`.
|
|
12
|
+
*/
|
|
13
|
+
declare const CREDIT_WARNING_DISMISS_SEGMENT = "credit-warning/dismiss";
|
|
14
|
+
/**
|
|
15
|
+
* Whether the in-app credit warning should show for this company.
|
|
16
|
+
*
|
|
17
|
+
* True only when the server sent the warning this period and nobody dismissed it,
|
|
18
|
+
* the company has AI and an active subscription, and the credits left are at or
|
|
19
|
+
* below `100 - thresholdPercent` percent of this period's grant. At 0 credits
|
|
20
|
+
* (or below) it is false: `OutOfCreditsBanner` owns that state.
|
|
21
|
+
*/
|
|
22
|
+
declare function isCreditWarningActive(company: CompanyInterface, thresholdPercent: number): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The subscription the `?action=change-plan` deep link opens the wizard on: the
|
|
25
|
+
* first active or trialing recurring subscription, in the dashboard's own order.
|
|
26
|
+
* `undefined` means there is none, and the subscribe wizard opens instead.
|
|
27
|
+
*/
|
|
28
|
+
declare function resolveChangePlanTarget(subscriptions: StripeSubscriptionInterface[]): StripeSubscriptionInterface | undefined;
|
|
3
29
|
|
|
4
30
|
type CompanyInput = {
|
|
5
31
|
id: string;
|
|
@@ -37,6 +63,15 @@ interface CompanyInterface extends ApiDataInterface {
|
|
|
37
63
|
* defaults an absent attribute to `true`, so the getter always has a value.
|
|
38
64
|
*/
|
|
39
65
|
get aiEnabled(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Credit-warning state, written by the server only (never sent by
|
|
68
|
+
* `createJsonApi()`). All four are absent until the feature is on and the
|
|
69
|
+
* company has received a monthly grant.
|
|
70
|
+
*/
|
|
71
|
+
get creditsGrantedThisPeriod(): number | undefined;
|
|
72
|
+
get creditWarningSentAt(): Date | undefined;
|
|
73
|
+
get creditWarningDismissedAt(): Date | undefined;
|
|
74
|
+
get creditWarningOffer(): CreditWarningOffer | undefined;
|
|
40
75
|
get features(): FeatureInterface[];
|
|
41
76
|
get modules(): ModuleInterface[];
|
|
42
77
|
get legal_address(): string | undefined;
|
|
@@ -99,4 +134,4 @@ interface UserInterface extends ApiDataInterface {
|
|
|
99
134
|
get modules(): ModuleInterface[];
|
|
100
135
|
}
|
|
101
136
|
|
|
102
|
-
export type
|
|
137
|
+
export { type CreditWarningOffer as C, type RoleInput as R, type UserInterface as U, CREDIT_WARNING_DISMISS_SEGMENT as a, type CompanyInput as b, type CompanyInterface as c, type RoleInterface as d, type UserInput as e, isCreditWarningActive as i, resolveChangePlanTarget as r };
|
package/messages/en.json
CHANGED
|
@@ -628,6 +628,14 @@
|
|
|
628
628
|
"subscribe": "Subscribe",
|
|
629
629
|
"title": "Confirm Your Subscription"
|
|
630
630
|
},
|
|
631
|
+
"credit_warning": {
|
|
632
|
+
"title": "You've used {percent}% of your AI credits.",
|
|
633
|
+
"body": "Everything except AI keeps working when they run out.",
|
|
634
|
+
"subscribe": "Subscribe",
|
|
635
|
+
"upgrade": "Upgrade your plan",
|
|
636
|
+
"add_pack": "Add a credit pack",
|
|
637
|
+
"dismiss": "Dismiss"
|
|
638
|
+
},
|
|
631
639
|
"dashboard": {
|
|
632
640
|
"errors": {
|
|
633
641
|
"load_customer": "Failed to load billing account",
|
package/messages/it.json
CHANGED
|
@@ -628,6 +628,14 @@
|
|
|
628
628
|
"subscribe": "Abbonati",
|
|
629
629
|
"title": "Conferma il tuo abbonamento"
|
|
630
630
|
},
|
|
631
|
+
"credit_warning": {
|
|
632
|
+
"title": "Hai usato l'{percent}% dei tuoi crediti IA.",
|
|
633
|
+
"body": "Quando finiscono, tutto continua a funzionare tranne l'IA.",
|
|
634
|
+
"subscribe": "Abbonati",
|
|
635
|
+
"upgrade": "Passa a un piano superiore",
|
|
636
|
+
"add_pack": "Aggiungi un pacchetto di crediti",
|
|
637
|
+
"dismiss": "Chiudi"
|
|
638
|
+
},
|
|
631
639
|
"dashboard": {
|
|
632
640
|
"errors": {
|
|
633
641
|
"load_customer": "Impossibile caricare l'account di fatturazione",
|
package/package.json
CHANGED
|
@@ -239,9 +239,14 @@ export function RoundPageContainer({
|
|
|
239
239
|
// between the shell and the content is dropped so the document is the only
|
|
240
240
|
// scroller; leaving a single `overflow-hidden` in that chain silently
|
|
241
241
|
// truncates the page instead of letting it grow.
|
|
242
|
+
//
|
|
243
|
+
// `--app-banner-h` is the height of an app banner drawn above the header (a
|
|
244
|
+
// demo bar, a credit warning). Unset, it is 0 and the height is unchanged; an
|
|
245
|
+
// app that renders such a banner publishes its live height on <html>, or the
|
|
246
|
+
// banner pushes the page past the viewport and the document scrolls it away.
|
|
242
247
|
const shellHeight = isFixed
|
|
243
|
-
? `h-[calc(100svh-var(--app-header-h,3rem))]`
|
|
244
|
-
: `min-h-[calc(100svh-var(--app-header-h,3rem))]`;
|
|
248
|
+
? `h-[calc(100svh-var(--app-header-h,3rem)-var(--app-banner-h,0px))]`
|
|
249
|
+
: `min-h-[calc(100svh-var(--app-header-h,3rem)-var(--app-banner-h,0px))]`;
|
|
245
250
|
const clip = isFixed ? `overflow-hidden` : ``;
|
|
246
251
|
const scrollY = isFixed ? `overflow-y-auto` : ``;
|
|
247
252
|
// The bar is an in-flow sibling of the content (see MobileNavigationBar's own
|
package/src/components/index.ts
CHANGED
|
@@ -24,6 +24,11 @@ export * from "../features/auth/components";
|
|
|
24
24
|
export * from "../features/administration";
|
|
25
25
|
export * from "../features/ai-connection";
|
|
26
26
|
// Billing components moved to separate entry point: @carlonicora/nextjs-jsonapi/billing
|
|
27
|
+
// Exception: the credit warning banner mounts on every page, so it ships from this
|
|
28
|
+
// Stripe-free entry. Its import chain must never reach `@stripe/*`.
|
|
29
|
+
export { CreditWarningBanner } from "../features/billing/components/widgets/CreditWarningBanner";
|
|
30
|
+
export type { CreditWarningBannerProps } from "../features/billing/components/widgets/CreditWarningBanner";
|
|
31
|
+
export { isCreditWarningActive, CREDIT_WARNING_DISMISS_SEGMENT } from "../features/billing/data/credit-warning";
|
|
27
32
|
export * from "../features/company/components";
|
|
28
33
|
export * from "../features/content/components";
|
|
29
34
|
export * from "../features/how-to/components";
|
|
@@ -14,6 +14,7 @@ import { SubscriptionsContainer } from "../../stripe-subscription/components";
|
|
|
14
14
|
import { SubscriptionWizard } from "../../stripe-subscription/components/wizards";
|
|
15
15
|
import { MeterInterface, MeterSummaryInterface, StripeUsageService } from "../../stripe-usage";
|
|
16
16
|
import { UsageContainer } from "../../stripe-usage/components";
|
|
17
|
+
import { resolveChangePlanTarget } from "../../data/credit-warning";
|
|
17
18
|
import {
|
|
18
19
|
BillingUsageSummaryCard,
|
|
19
20
|
CustomerInfoCard,
|
|
@@ -89,6 +90,10 @@ export function BillingDashboardContainer() {
|
|
|
89
90
|
const [showWizard, setShowWizard] = useState(false);
|
|
90
91
|
const [editingSubscription, setEditingSubscription] = useState<StripeSubscriptionInterface | null>(null);
|
|
91
92
|
|
|
93
|
+
// `?action=change-plan` needs the subscriptions to pick the wizard's target, so the
|
|
94
|
+
// URL effect only records the intent and a second effect acts on it once they load.
|
|
95
|
+
const [pendingChangePlan, setPendingChangePlan] = useState(false);
|
|
96
|
+
|
|
92
97
|
// Check if company has metered subscriptions
|
|
93
98
|
const hasMeteredSubscriptions = useCallback((): boolean => {
|
|
94
99
|
return data.subscriptions.some((sub) => sub.price?.recurring?.usageType === "metered");
|
|
@@ -286,11 +291,13 @@ export function BillingDashboardContainer() {
|
|
|
286
291
|
// Handle URL action parameter for deep linking. `?action=subscribe` records the
|
|
287
292
|
// intent to subscribe; the wizard itself only renders once a customer exists, so a
|
|
288
293
|
// company without one sees the setup card first and the wizard opens right after
|
|
289
|
-
// "Set Up Billing Account" succeeds.
|
|
294
|
+
// "Set Up Billing Account" succeeds. `?action=change-plan` records a pending intent
|
|
295
|
+
// that the effect below resolves once the subscriptions have loaded.
|
|
290
296
|
useEffect(() => {
|
|
291
297
|
const action = searchParams.get("action");
|
|
292
|
-
if (action === "subscribe") {
|
|
293
|
-
setShowWizard(true);
|
|
298
|
+
if (action === "subscribe" || action === "change-plan") {
|
|
299
|
+
if (action === "subscribe") setShowWizard(true);
|
|
300
|
+
else setPendingChangePlan(true);
|
|
294
301
|
// Drop only `action`, so a refresh does not re-trigger it. Every other parameter
|
|
295
302
|
// must survive: a host page (the settings hub) selects this dashboard through
|
|
296
303
|
// `?section=`, and losing it switches the page away and unmounts this component.
|
|
@@ -307,6 +314,16 @@ export function BillingDashboardContainer() {
|
|
|
307
314
|
}
|
|
308
315
|
}, [searchParams]);
|
|
309
316
|
|
|
317
|
+
// Act on a pending `?action=change-plan` once the subscriptions fetch has finished:
|
|
318
|
+
// open the wizard on the active or trialing recurring subscription, or the subscribe
|
|
319
|
+
// wizard when there is none. The intent is cleared in the same step, so the reload
|
|
320
|
+
// after the wizard closes does not open it again.
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (!pendingChangePlan || loading.subscriptions) return;
|
|
323
|
+
setPendingChangePlan(false);
|
|
324
|
+
handleOpenWizard(resolveChangePlanTarget(data.subscriptions));
|
|
325
|
+
}, [pendingChangePlan, loading.subscriptions, data.subscriptions, handleOpenWizard]);
|
|
326
|
+
|
|
310
327
|
// Detect critical subscriptions for alert banners
|
|
311
328
|
const criticalSubscriptions = data.subscriptions.filter(
|
|
312
329
|
(sub) =>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Exported from the Stripe-free `components` entry, because the banner mounts on
|
|
4
|
+
// every page. Keep every import here (and everything they import) free of `@stripe/*`.
|
|
5
|
+
|
|
6
|
+
import { XIcon } from "lucide-react";
|
|
7
|
+
import { useTranslations } from "next-intl";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { Alert, AlertAction, AlertDescription, AlertTitle } from "../../../../shadcnui/ui/alert";
|
|
10
|
+
import { Button } from "../../../../shadcnui/ui/button";
|
|
11
|
+
import { Link } from "../../../../shadcnui/custom/link";
|
|
12
|
+
import { CompanyService } from "../../../company/data/company.service";
|
|
13
|
+
import { useCurrentUserContext } from "../../../user/contexts/CurrentUserContext";
|
|
14
|
+
import { isCreditWarningActive } from "../../data/credit-warning";
|
|
15
|
+
|
|
16
|
+
export type CreditWarningBannerProps = {
|
|
17
|
+
/** Only company administrators see the banner and can dismiss it. */
|
|
18
|
+
canManageBilling: boolean;
|
|
19
|
+
/** Must match the API's `billing.creditWarning.thresholdPercent`. Defaults to 80. */
|
|
20
|
+
thresholdPercent?: number;
|
|
21
|
+
changePlanHref?: string;
|
|
22
|
+
buyCreditsHref?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Shown once per billing period when the credits left fall to `100 - thresholdPercent`
|
|
27
|
+
* percent of this period's grant. Hidden at 0 credits, where the app's out-of-credits
|
|
28
|
+
* banner takes over. The offer the server picked decides which links show.
|
|
29
|
+
*/
|
|
30
|
+
export function CreditWarningBanner({
|
|
31
|
+
canManageBilling,
|
|
32
|
+
thresholdPercent = 80,
|
|
33
|
+
changePlanHref = "/settings/billing?action=change-plan",
|
|
34
|
+
buyCreditsHref = "/settings/billing?action=subscribe",
|
|
35
|
+
}: CreditWarningBannerProps) {
|
|
36
|
+
const t = useTranslations();
|
|
37
|
+
// `company` is the live one, with the `company:credits_updated` balances applied.
|
|
38
|
+
const { company, refreshUser } = useCurrentUserContext();
|
|
39
|
+
const [dismissing, setDismissing] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Checked after every hook, so the hook count never changes between renders.
|
|
42
|
+
if (!company || !canManageBilling || !isCreditWarningActive(company, thresholdPercent)) return null;
|
|
43
|
+
|
|
44
|
+
const offer = company.creditWarningOffer;
|
|
45
|
+
|
|
46
|
+
const handleDismiss = async () => {
|
|
47
|
+
setDismissing(true);
|
|
48
|
+
try {
|
|
49
|
+
await CompanyService.dismissCreditWarning({ companyId: company.id });
|
|
50
|
+
await refreshUser({ skipCookieUpdate: true });
|
|
51
|
+
} finally {
|
|
52
|
+
setDismissing(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Alert className="mb-4">
|
|
58
|
+
<AlertTitle>{t("billing.ui.credit_warning.title", { percent: thresholdPercent })}</AlertTitle>
|
|
59
|
+
<AlertDescription>
|
|
60
|
+
<p>{t("billing.ui.credit_warning.body")}</p>
|
|
61
|
+
{/* Never underlined: colour and weight carry the affordance. */}
|
|
62
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
63
|
+
{offer === "subscribe" && (
|
|
64
|
+
<Link href={changePlanHref} className="text-primary font-medium">
|
|
65
|
+
{t("billing.ui.credit_warning.subscribe")}
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
68
|
+
{offer === "upgrade-or-pack" && (
|
|
69
|
+
<Link href={changePlanHref} className="text-primary font-medium">
|
|
70
|
+
{t("billing.ui.credit_warning.upgrade")}
|
|
71
|
+
</Link>
|
|
72
|
+
)}
|
|
73
|
+
{(offer === "upgrade-or-pack" || offer === "pack") && (
|
|
74
|
+
<Link href={buyCreditsHref} className="text-primary font-medium">
|
|
75
|
+
{t("billing.ui.credit_warning.add_pack")}
|
|
76
|
+
</Link>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</AlertDescription>
|
|
80
|
+
<AlertAction>
|
|
81
|
+
<Button
|
|
82
|
+
variant="ghost"
|
|
83
|
+
size="icon-xs"
|
|
84
|
+
aria-label={t("billing.ui.credit_warning.dismiss")}
|
|
85
|
+
disabled={dismissing}
|
|
86
|
+
onClick={handleDismiss}
|
|
87
|
+
>
|
|
88
|
+
<XIcon />
|
|
89
|
+
</Button>
|
|
90
|
+
</AlertAction>
|
|
91
|
+
</Alert>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { CompanyInterface } from "../../company/data/company.interface";
|
|
3
|
+
import { SubscriptionStatus } from "../stripe-subscription/data/stripe-subscription.interface";
|
|
4
|
+
import type { StripeSubscriptionInterface } from "../stripe-subscription/data/stripe-subscription.interface";
|
|
5
|
+
import { CREDIT_WARNING_DISMISS_SEGMENT, isCreditWarningActive, resolveChangePlanTarget } from "./credit-warning";
|
|
6
|
+
|
|
7
|
+
const makeCompany = (overrides: Record<string, unknown> = {}): CompanyInterface =>
|
|
8
|
+
({
|
|
9
|
+
aiEnabled: true,
|
|
10
|
+
isActiveSubscription: true,
|
|
11
|
+
creditsGrantedThisPeriod: 200,
|
|
12
|
+
creditWarningSentAt: new Date("2026-10-25T10:00:00.000Z"),
|
|
13
|
+
creditWarningDismissedAt: undefined,
|
|
14
|
+
availableMonthlyCredits: 30,
|
|
15
|
+
availableExtraCredits: 0,
|
|
16
|
+
...overrides,
|
|
17
|
+
}) as unknown as CompanyInterface;
|
|
18
|
+
|
|
19
|
+
const makeSubscription = (
|
|
20
|
+
id: string,
|
|
21
|
+
status: SubscriptionStatus,
|
|
22
|
+
priceType: "one_time" | "recurring",
|
|
23
|
+
): StripeSubscriptionInterface => ({ id, status, price: { priceType } }) as unknown as StripeSubscriptionInterface;
|
|
24
|
+
|
|
25
|
+
describe("isCreditWarningActive (threshold 80)", () => {
|
|
26
|
+
it("is true with 30 of 200 left", () => {
|
|
27
|
+
expect(isCreditWarningActive(makeCompany({ availableMonthlyCredits: 30 }), 80)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("is true at exactly 80% used (40 of 200 left)", () => {
|
|
31
|
+
expect(isCreditWarningActive(makeCompany({ availableMonthlyCredits: 40 }), 80)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("is false at 0 credits, which is the out-of-credits banner's job", () => {
|
|
35
|
+
expect(isCreditWarningActive(makeCompany({ availableMonthlyCredits: 0, availableExtraCredits: 0 }), 80)).toBe(
|
|
36
|
+
false,
|
|
37
|
+
);
|
|
38
|
+
expect(isCreditWarningActive(makeCompany({ availableMonthlyCredits: 0, availableExtraCredits: -0.3 }), 80)).toBe(
|
|
39
|
+
false,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("is false when monthly plus extra is above the threshold (50 > 40)", () => {
|
|
44
|
+
expect(isCreditWarningActive(makeCompany({ availableMonthlyCredits: 30, availableExtraCredits: 20 }), 80)).toBe(
|
|
45
|
+
false,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("is false once dismissed", () => {
|
|
50
|
+
expect(isCreditWarningActive(makeCompany({ creditWarningDismissedAt: new Date() }), 80)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("is false when the warning was never sent", () => {
|
|
54
|
+
expect(isCreditWarningActive(makeCompany({ creditWarningSentAt: undefined }), 80)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is false without an active subscription", () => {
|
|
58
|
+
expect(isCreditWarningActive(makeCompany({ isActiveSubscription: false }), 80)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("is false when AI is off", () => {
|
|
62
|
+
expect(isCreditWarningActive(makeCompany({ aiEnabled: false }), 80)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("is false when nothing was granted this period", () => {
|
|
66
|
+
expect(isCreditWarningActive(makeCompany({ creditsGrantedThisPeriod: undefined }), 80)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("CREDIT_WARNING_DISMISS_SEGMENT", () => {
|
|
71
|
+
it("matches the backend segment", () => {
|
|
72
|
+
expect(CREDIT_WARNING_DISMISS_SEGMENT).toBe("credit-warning/dismiss");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("resolveChangePlanTarget", () => {
|
|
77
|
+
it("picks the trialing recurring subscription over a one-time one", () => {
|
|
78
|
+
const oneTime = makeSubscription("one-time", SubscriptionStatus.ACTIVE, "one_time");
|
|
79
|
+
const trialing = makeSubscription("trialing", SubscriptionStatus.TRIALING, "recurring");
|
|
80
|
+
|
|
81
|
+
expect(resolveChangePlanTarget([oneTime, trialing])).toBe(trialing);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns undefined when the only recurring subscription is canceled", () => {
|
|
85
|
+
const canceled = makeSubscription("canceled", SubscriptionStatus.CANCELED, "recurring");
|
|
86
|
+
|
|
87
|
+
expect(resolveChangePlanTarget([canceled])).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns the first active recurring subscription in array order", () => {
|
|
91
|
+
const first = makeSubscription("first", SubscriptionStatus.ACTIVE, "recurring");
|
|
92
|
+
const second = makeSubscription("second", SubscriptionStatus.ACTIVE, "recurring");
|
|
93
|
+
|
|
94
|
+
expect(resolveChangePlanTarget([first, second])).toBe(first);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Credit warning: shared contract with the backend and pure helpers.
|
|
2
|
+
// Keep this file free of any `@stripe/*` import: it is re-exported from the
|
|
3
|
+
// Stripe-free `components` entry, because the banner mounts on every page.
|
|
4
|
+
|
|
5
|
+
import type { CompanyInterface } from "../../company/data/company.interface";
|
|
6
|
+
import { SubscriptionStatus } from "../stripe-subscription/data/stripe-subscription.interface";
|
|
7
|
+
import type { StripeSubscriptionInterface } from "../stripe-subscription/data/stripe-subscription.interface";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The offer the server chose when it sent this period's warning.
|
|
11
|
+
* Mirrors the backend `CreditWarningOffer`.
|
|
12
|
+
*/
|
|
13
|
+
export type CreditWarningOffer = "subscribe" | "upgrade-or-pack" | "pack";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Child segment of the dismiss route: `POST companies/:companyId/credit-warning/dismiss`.
|
|
17
|
+
* Must match the backend `CREDIT_WARNING_DISMISS_SEGMENT`.
|
|
18
|
+
*/
|
|
19
|
+
export const CREDIT_WARNING_DISMISS_SEGMENT = "credit-warning/dismiss";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether the in-app credit warning should show for this company.
|
|
23
|
+
*
|
|
24
|
+
* True only when the server sent the warning this period and nobody dismissed it,
|
|
25
|
+
* the company has AI and an active subscription, and the credits left are at or
|
|
26
|
+
* below `100 - thresholdPercent` percent of this period's grant. At 0 credits
|
|
27
|
+
* (or below) it is false: `OutOfCreditsBanner` owns that state.
|
|
28
|
+
*/
|
|
29
|
+
export function isCreditWarningActive(company: CompanyInterface, thresholdPercent: number): boolean {
|
|
30
|
+
if (!company.aiEnabled || !company.isActiveSubscription) return false;
|
|
31
|
+
if (!company.creditWarningSentAt || company.creditWarningDismissedAt) return false;
|
|
32
|
+
|
|
33
|
+
const granted = company.creditsGrantedThisPeriod;
|
|
34
|
+
if (granted === undefined || granted <= 0) return false;
|
|
35
|
+
|
|
36
|
+
const monthly = company.availableMonthlyCredits;
|
|
37
|
+
const extra = company.availableExtraCredits;
|
|
38
|
+
// Same "has credits" test as useHasCredits / CompanyService.hasAvailableCredits.
|
|
39
|
+
if (!(monthly > 0 || extra > 0)) return false;
|
|
40
|
+
|
|
41
|
+
return monthly + extra <= (granted * (100 - thresholdPercent)) / 100;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The subscription the `?action=change-plan` deep link opens the wizard on: the
|
|
46
|
+
* first active or trialing recurring subscription, in the dashboard's own order.
|
|
47
|
+
* `undefined` means there is none, and the subscribe wizard opens instead.
|
|
48
|
+
*/
|
|
49
|
+
export function resolveChangePlanTarget(
|
|
50
|
+
subscriptions: StripeSubscriptionInterface[],
|
|
51
|
+
): StripeSubscriptionInterface | undefined {
|
|
52
|
+
return subscriptions.find(
|
|
53
|
+
(sub) =>
|
|
54
|
+
(sub.status === SubscriptionStatus.ACTIVE || sub.status === SubscriptionStatus.TRIALING) &&
|
|
55
|
+
sub.price?.priceType === "recurring",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -397,4 +397,10 @@ export const BILLING_UI_I18N_KEYS = [
|
|
|
397
397
|
"billing.ui.wizard.review.use_another_card",
|
|
398
398
|
"billing.ui.wizard.steps.select_plan",
|
|
399
399
|
"billing.ui.wizard.subscribe_description",
|
|
400
|
+
"billing.ui.credit_warning.title",
|
|
401
|
+
"billing.ui.credit_warning.body",
|
|
402
|
+
"billing.ui.credit_warning.subscribe",
|
|
403
|
+
"billing.ui.credit_warning.upgrade",
|
|
404
|
+
"billing.ui.credit_warning.add_pack",
|
|
405
|
+
"billing.ui.credit_warning.dismiss",
|
|
400
406
|
] as const;
|