@carlonicora/nextjs-jsonapi 6.29.1 → 6.31.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 (92) hide show
  1. package/dist/{BlockNoteEditor-K4HWQLPJ.js → BlockNoteEditor-ICGGCW6N.js} +24 -24
  2. package/dist/{BlockNoteEditor-K4HWQLPJ.js.map → BlockNoteEditor-ICGGCW6N.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-TDX2RC7I.mjs → BlockNoteEditor-O4E67ECL.mjs} +4 -4
  4. package/dist/{BlockNoteViewer-76CKL6W5.mjs → BlockNoteViewer-BNZEXGVT.mjs} +2 -2
  5. package/dist/{BlockNoteViewer-B25WKYQP.js → BlockNoteViewer-YONLBNXW.js} +3 -3
  6. package/dist/{BlockNoteViewer-B25WKYQP.js.map → BlockNoteViewer-YONLBNXW.js.map} +1 -1
  7. package/dist/billing/index.js +465 -465
  8. package/dist/billing/index.mjs +3 -3
  9. package/dist/{chunk-77JC2UCF.js → chunk-5XP5G45B.js} +23 -10
  10. package/dist/chunk-5XP5G45B.js.map +1 -0
  11. package/dist/{chunk-OVSR72CZ.mjs → chunk-BT3VFB3I.mjs} +333 -299
  12. package/dist/chunk-BT3VFB3I.mjs.map +1 -0
  13. package/dist/{chunk-7MGXQIEP.mjs → chunk-FDKAVSZ6.mjs} +2 -2
  14. package/dist/{chunk-FJUKLVVX.mjs → chunk-HNK2O6YE.mjs} +23 -10
  15. package/dist/chunk-HNK2O6YE.mjs.map +1 -0
  16. package/dist/{chunk-M7YK5CCE.mjs → chunk-JL6XL5NK.mjs} +9 -2
  17. package/dist/chunk-JL6XL5NK.mjs.map +1 -0
  18. package/dist/{chunk-QI4SCGPB.js → chunk-M7FA63QE.js} +803 -769
  19. package/dist/chunk-M7FA63QE.js.map +1 -0
  20. package/dist/{chunk-F3EVYBMA.js → chunk-NQL2NDKH.js} +4 -4
  21. package/dist/{chunk-F3EVYBMA.js.map → chunk-NQL2NDKH.js.map} +1 -1
  22. package/dist/{chunk-ONU4WCDG.js → chunk-Q5SMFSE4.js} +11 -4
  23. package/dist/chunk-Q5SMFSE4.js.map +1 -0
  24. package/dist/client/index.js +4 -4
  25. package/dist/client/index.mjs +3 -3
  26. package/dist/components/forms/CommonEditorButtons.d.ts.map +1 -1
  27. package/dist/components/index.js +4 -4
  28. package/dist/components/index.mjs +3 -3
  29. package/dist/contexts/index.js +4 -4
  30. package/dist/contexts/index.mjs +3 -3
  31. package/dist/core/index.js +2 -2
  32. package/dist/core/index.mjs +1 -1
  33. package/dist/email-automations/index.js +105 -105
  34. package/dist/email-automations/index.mjs +3 -3
  35. package/dist/features/analytics/index.js +114 -114
  36. package/dist/features/analytics/index.mjs +3 -3
  37. package/dist/features/analytics/server.js +4 -4
  38. package/dist/features/analytics/server.mjs +2 -2
  39. package/dist/features/auth/components/GdprConsentSection.d.ts +2 -1
  40. package/dist/features/auth/components/GdprConsentSection.d.ts.map +1 -1
  41. package/dist/features/auth/components/forms/Login.d.ts.map +1 -1
  42. package/dist/features/auth/components/forms/Register.d.ts.map +1 -1
  43. package/dist/features/auth/data/auth.service.d.ts +8 -3
  44. package/dist/features/auth/data/auth.service.d.ts.map +1 -1
  45. package/dist/features/auth/data/pending-registration.d.ts +2 -0
  46. package/dist/features/auth/data/pending-registration.d.ts.map +1 -1
  47. package/dist/features/auth/data/pending-registration.interface.d.ts +1 -0
  48. package/dist/features/auth/data/pending-registration.interface.d.ts.map +1 -1
  49. package/dist/features/auth/hooks/useCompleteLogin.d.ts +9 -0
  50. package/dist/features/auth/hooks/useCompleteLogin.d.ts.map +1 -0
  51. package/dist/features/blog/index.js +129 -129
  52. package/dist/features/blog/index.mjs +3 -3
  53. package/dist/features/blog/server.js +7 -7
  54. package/dist/features/blog/server.mjs +1 -1
  55. package/dist/features/help/index.js +37 -37
  56. package/dist/features/help/index.mjs +3 -3
  57. package/dist/features/tokenusage/index.js +114 -114
  58. package/dist/features/tokenusage/index.mjs +3 -3
  59. package/dist/index.js +3 -3
  60. package/dist/index.mjs +2 -2
  61. package/dist/login/config.d.ts +3 -0
  62. package/dist/login/config.d.ts.map +1 -1
  63. package/dist/login/index.d.ts +1 -1
  64. package/dist/login/index.d.ts.map +1 -1
  65. package/dist/server/index.js +3 -3
  66. package/dist/server/index.mjs +2 -2
  67. package/messages/en.json +1 -0
  68. package/messages/it.json +1 -0
  69. package/package.json +1 -1
  70. package/src/components/forms/CommonEditorButtons.tsx +16 -2
  71. package/src/features/auth/components/GdprConsentSection.tsx +3 -1
  72. package/src/features/auth/components/forms/Login.tsx +4 -32
  73. package/src/features/auth/components/forms/Register.tsx +66 -36
  74. package/src/features/auth/data/__tests__/auth.oauth-completion.spec.ts +44 -0
  75. package/src/features/auth/data/__tests__/pending-registration.spec.ts +8 -0
  76. package/src/features/auth/data/auth.service.ts +21 -15
  77. package/src/features/auth/data/pending-registration.interface.ts +1 -0
  78. package/src/features/auth/data/pending-registration.ts +6 -0
  79. package/src/features/auth/hooks/__tests__/useCompleteLogin.spec.tsx +85 -0
  80. package/src/features/auth/hooks/useCompleteLogin.ts +47 -0
  81. package/src/login/__tests__/config.spec.ts +21 -0
  82. package/src/login/config.ts +8 -0
  83. package/src/login/index.ts +1 -0
  84. package/dist/chunk-77JC2UCF.js.map +0 -1
  85. package/dist/chunk-FJUKLVVX.mjs.map +0 -1
  86. package/dist/chunk-M7YK5CCE.mjs.map +0 -1
  87. package/dist/chunk-ONU4WCDG.js.map +0 -1
  88. package/dist/chunk-OVSR72CZ.mjs.map +0 -1
  89. package/dist/chunk-QI4SCGPB.js.map +0 -1
  90. /package/dist/{BlockNoteEditor-TDX2RC7I.mjs.map → BlockNoteEditor-O4E67ECL.mjs.map} +0 -0
  91. /package/dist/{BlockNoteViewer-76CKL6W5.mjs.map → BlockNoteViewer-BNZEXGVT.mjs.map} +0 -0
  92. /package/dist/{chunk-7MGXQIEP.mjs.map → chunk-FDKAVSZ6.mjs.map} +0 -0
@@ -0,0 +1,44 @@
1
+ import { beforeAll, describe, expect, it } from "vitest";
2
+ import { Modules } from "../../../../core";
3
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
4
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
5
+ import { Auth } from "../auth";
6
+
7
+ // Auth.createJsonApi() reads Modules.Auth.name through the real ModuleRegistry,
8
+ // so it must be registered (same pattern as auth.spec.ts).
9
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
10
+ try {
11
+ ModuleRegistry.get(key as any);
12
+ } catch {
13
+ ModuleRegistry.register(key, module);
14
+ }
15
+ };
16
+
17
+ beforeAll(() => {
18
+ registerIfAbsent("Auth", { name: "auth", model: Auth } as any);
19
+ registerIfAbsent("User", { name: "users", model: class {} } as any);
20
+ });
21
+
22
+ describe("Auth.createJsonApi() for the OAuth completion", () => {
23
+ it("an OAuth completion body carries the pending id and the given consent", () => {
24
+ const body = new Auth().createJsonApi({
25
+ id: "p1",
26
+ termsAcceptedAt: "2026-11-09T00:00:00.000Z",
27
+ marketingConsent: false,
28
+ });
29
+
30
+ expect(body).toEqual({
31
+ data: {
32
+ type: Modules.Auth.name,
33
+ id: "p1",
34
+ attributes: { termsAcceptedAt: "2026-11-09T00:00:00.000Z", marketingConsent: false },
35
+ },
36
+ });
37
+ });
38
+
39
+ it("an invite completion sends no consent attributes", () => {
40
+ const body = new Auth().createJsonApi({ id: "p1" });
41
+
42
+ expect(body).toEqual({ data: { type: Modules.Auth.name, id: "p1", attributes: {} } });
43
+ });
44
+ });
@@ -16,6 +16,7 @@ describe("PendingRegistration.rehydrate()", () => {
16
16
  provider: "discord",
17
17
  hasPass: true,
18
18
  emailInUse: true,
19
+ hasInvite: true,
19
20
  });
20
21
 
21
22
  expect(pending.id).toBe("pending-1");
@@ -25,6 +26,7 @@ describe("PendingRegistration.rehydrate()", () => {
25
26
  expect(pending.provider).toBe("discord");
26
27
  expect(pending.hasPass).toBe(true);
27
28
  expect(pending.emailInUse).toBe(true);
29
+ expect(pending.hasInvite).toBe(true);
28
30
  });
29
31
 
30
32
  it("leaves avatar undefined and defaults hasPass/emailInUse to false when absent", () => {
@@ -34,4 +36,10 @@ describe("PendingRegistration.rehydrate()", () => {
34
36
  expect(pending.hasPass).toBe(false);
35
37
  expect(pending.emailInUse).toBe(false);
36
38
  });
39
+
40
+ it("PendingRegistration rehydrates hasInvite with a false default", () => {
41
+ expect(hydrate({ name: "Aria", email: "aria@example.com", provider: "google" }).hasInvite).toBe(false);
42
+ expect(hydrate({ name: "Aria", hasInvite: null }).hasInvite).toBe(false);
43
+ expect(hydrate({ name: "Aria", hasInvite: true }).hasInvite).toBe(true);
44
+ });
37
45
  });
@@ -195,24 +195,30 @@ export class AuthService extends AbstractService {
195
195
  });
196
196
  }
197
197
 
198
+ /**
199
+ * Completes an OAuth sign-up waiting for consent. A sign-up with a usable
200
+ * waitlist invite sends no consent: the server copies it from the waitlist.
201
+ * Throws an Error with message `"<status>:<message>"` on failure.
202
+ */
198
203
  static async completeOAuthRegistration(params: {
199
204
  pendingId: string;
200
- termsAcceptedAt: string;
201
- marketingConsent: boolean;
202
- marketingConsentAt: string | null;
205
+ termsAcceptedAt?: string;
206
+ marketingConsent?: boolean;
207
+ marketingConsentAt?: string | null;
203
208
  }): Promise<{ code: string }> {
204
- const endpoint = new EndpointCreator({ endpoint: Modules.Auth, id: "oauth", childEndpoint: "complete" });
205
-
206
- const response: ApiResponseInterface = await JsonApiPost({
207
- classKey: Modules.Auth,
208
- endpoint: endpoint.generate(),
209
- body: params,
210
- overridesJsonApiCreation: true,
211
- language: "en",
209
+ const input: AuthInput = {
210
+ id: params.pendingId,
211
+ termsAcceptedAt: params.termsAcceptedAt,
212
+ marketingConsent: params.marketingConsent,
213
+ marketingConsentAt: params.marketingConsentAt,
214
+ };
215
+
216
+ return this.callApi<{ code: string }>({
217
+ type: Modules.Auth,
218
+ method: HttpMethod.POST,
219
+ endpoint: new EndpointCreator({ endpoint: Modules.Auth, id: "oauth", childEndpoint: "complete" }).generate(),
220
+ input: input,
221
+ suppressGlobalError: true,
212
222
  });
213
-
214
- if (!response.ok) throw new Error(response.error);
215
-
216
- return response.data as unknown as { code: string };
217
223
  }
218
224
  }
@@ -7,4 +7,5 @@ export interface PendingRegistrationInterface extends ApiDataInterface {
7
7
  get provider(): string;
8
8
  get hasPass(): boolean;
9
9
  get emailInUse(): boolean;
10
+ get hasInvite(): boolean;
10
11
  }
@@ -8,6 +8,7 @@ export class PendingRegistration extends AbstractApiData implements PendingRegis
8
8
  private _provider?: string;
9
9
  private _hasPass?: boolean;
10
10
  private _emailInUse?: boolean;
11
+ private _hasInvite?: boolean;
11
12
 
12
13
  get name(): string {
13
14
  return this._name ?? "";
@@ -33,6 +34,10 @@ export class PendingRegistration extends AbstractApiData implements PendingRegis
33
34
  return this._emailInUse ?? false;
34
35
  }
35
36
 
37
+ get hasInvite(): boolean {
38
+ return this._hasInvite ?? false;
39
+ }
40
+
36
41
  rehydrate(data: JsonApiHydratedDataInterface): this {
37
42
  super.rehydrate(data);
38
43
  this._name = data.jsonApi.attributes.name;
@@ -41,6 +46,7 @@ export class PendingRegistration extends AbstractApiData implements PendingRegis
41
46
  this._provider = data.jsonApi.attributes.provider;
42
47
  this._hasPass = data.jsonApi.attributes.hasPass ?? false;
43
48
  this._emailInUse = data.jsonApi.attributes.emailInUse ?? false;
49
+ this._hasInvite = data.jsonApi.attributes.hasInvite ?? false;
44
50
  return this;
45
51
  }
46
52
 
@@ -0,0 +1,85 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AuthComponent } from "../../enums";
4
+ import { useCompleteLogin } from "../useCompleteLogin";
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ setUser: vi.fn(),
8
+ setComponentType: vi.fn(),
9
+ setPendingTwoFactor: vi.fn(),
10
+ i18nRouter: { replace: vi.fn(), push: vi.fn() },
11
+ nativeRouter: { replace: vi.fn(), push: vi.fn() },
12
+ search: { value: "" },
13
+ }));
14
+
15
+ vi.mock(
16
+ "../../../user/contexts",
17
+ () => new Proxy({ useCurrentUserContext: () => ({ setUser: mocks.setUser }) }, { has: () => true }),
18
+ );
19
+
20
+ vi.mock(
21
+ "../../contexts",
22
+ () =>
23
+ new Proxy(
24
+ {
25
+ useAuthContext: () => ({
26
+ setComponentType: mocks.setComponentType,
27
+ setPendingTwoFactor: mocks.setPendingTwoFactor,
28
+ }),
29
+ },
30
+ { has: () => true },
31
+ ),
32
+ );
33
+
34
+ vi.mock(
35
+ "../../../../hooks",
36
+ () =>
37
+ new Proxy(
38
+ {
39
+ usePageUrlGenerator:
40
+ () =>
41
+ ({ page }: { page: string }) =>
42
+ page,
43
+ useI18nRouter: () => mocks.i18nRouter,
44
+ },
45
+ { has: () => true },
46
+ ),
47
+ );
48
+
49
+ vi.mock("next/navigation", () => ({
50
+ useRouter: () => mocks.nativeRouter,
51
+ useSearchParams: () => new URLSearchParams(mocks.search.value),
52
+ }));
53
+
54
+ describe("useCompleteLogin", () => {
55
+ beforeEach(() => {
56
+ vi.clearAllMocks();
57
+ mocks.search.value = "";
58
+ });
59
+
60
+ it("a user response sets the user and goes home", () => {
61
+ const { result } = renderHook(() => useCompleteLogin());
62
+ const user = { id: "u1" } as any;
63
+ result.current(user);
64
+ expect(mocks.setUser).toHaveBeenCalledWith(user);
65
+ expect(mocks.i18nRouter.replace).toHaveBeenCalledWith("/");
66
+ expect(mocks.nativeRouter.replace).not.toHaveBeenCalled();
67
+ });
68
+
69
+ it("a callbackUrl wins over home", () => {
70
+ mocks.search.value = "callbackUrl=/it/campaigns";
71
+ const { result } = renderHook(() => useCompleteLogin());
72
+ result.current({ id: "u1" } as any);
73
+ expect(mocks.nativeRouter.replace).toHaveBeenCalledWith("/it/campaigns");
74
+ expect(mocks.i18nRouter.replace).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("a two-factor challenge opens the challenge", () => {
78
+ const { result } = renderHook(() => useCompleteLogin());
79
+ const challenge = { pendingToken: "p" } as any;
80
+ result.current(challenge);
81
+ expect(mocks.setPendingTwoFactor).toHaveBeenCalledWith(challenge);
82
+ expect(mocks.setComponentType).toHaveBeenCalledWith(AuthComponent.TwoFactorChallenge);
83
+ expect(mocks.setUser).not.toHaveBeenCalled();
84
+ });
85
+ });
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useRouter, useSearchParams } from "next/navigation";
4
+ import { useCallback } from "react";
5
+ import { useI18nRouter, usePageUrlGenerator } from "../../../hooks";
6
+ import { UserInterface } from "../../user";
7
+ import { useCurrentUserContext } from "../../user/contexts";
8
+ import { useAuthContext } from "../contexts";
9
+ import { TwoFactorChallengeInterface } from "../data/two-factor-challenge.interface";
10
+ import { AuthComponent } from "../enums";
11
+
12
+ /**
13
+ * Post-login handling shared by Login and the invite sign-up in Register:
14
+ * opens the two-factor challenge when one is pending, otherwise stores the
15
+ * user and redirects to the callback URL or home.
16
+ */
17
+ export function useCompleteLogin(): (response: UserInterface | TwoFactorChallengeInterface) => void {
18
+ const { setUser } = useCurrentUserContext<UserInterface>();
19
+ const { setComponentType, setPendingTwoFactor } = useAuthContext();
20
+ const generateUrl = usePageUrlGenerator();
21
+ const i18nRouter = useI18nRouter();
22
+ const nativeRouter = useRouter(); // For URLs that already include locale
23
+ const searchParams = useSearchParams();
24
+ const callbackUrl = searchParams.get("callbackUrl");
25
+
26
+ return useCallback(
27
+ (response: UserInterface | TwoFactorChallengeInterface) => {
28
+ // Check if 2FA is required (response is TwoFactorChallengeInterface)
29
+ if ("pendingToken" in response) {
30
+ setPendingTwoFactor(response as TwoFactorChallengeInterface);
31
+ setComponentType(AuthComponent.TwoFactorChallenge);
32
+ return;
33
+ }
34
+
35
+ setUser(response as UserInterface);
36
+
37
+ // Use native router for callbackUrl since it already includes the locale prefix
38
+ // Use i18n router for normal navigation which adds locale automatically
39
+ if (callbackUrl) {
40
+ nativeRouter.replace(callbackUrl);
41
+ } else {
42
+ i18nRouter.replace(generateUrl({ page: `/` }));
43
+ }
44
+ },
45
+ [setUser, setComponentType, setPendingTwoFactor, generateUrl, i18nRouter, nativeRouter, callbackUrl],
46
+ );
47
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { configureLogin, isCompanyNameRequired } from "../index";
3
+
4
+ describe("login config — requireCompanyName", () => {
5
+ it("isCompanyNameRequired defaults to true", () => {
6
+ configureLogin({});
7
+ expect(isCompanyNameRequired()).toBe(true);
8
+ });
9
+
10
+ it("requireCompanyName false hides the company field", () => {
11
+ configureLogin({ requireCompanyName: false });
12
+ expect(isCompanyNameRequired()).toBe(false);
13
+ });
14
+
15
+ it("a later configureLogin without the option restores the default", () => {
16
+ configureLogin({ requireCompanyName: false });
17
+ expect(isCompanyNameRequired()).toBe(false);
18
+ configureLogin({});
19
+ expect(isCompanyNameRequired()).toBe(true);
20
+ });
21
+ });
@@ -3,6 +3,7 @@ let _useGoogleAuth: boolean = false;
3
3
  let _useInternalAuth: boolean = true;
4
4
  let _allowRegistration: boolean = true;
5
5
  let _registrationMode: "open" | "closed" | "waitlist" = "open";
6
+ let _requireCompanyName: boolean = true;
6
7
 
7
8
  export type RegistrationMode = "open" | "closed" | "waitlist";
8
9
 
@@ -12,6 +13,8 @@ export interface LoginConfig {
12
13
  useInternalAuth?: boolean;
13
14
  allowRegistration?: boolean;
14
15
  registrationMode?: RegistrationMode;
16
+ /** Show and require the company name on sign-up. Default `true`. */
17
+ requireCompanyName?: boolean;
15
18
  }
16
19
 
17
20
  export function configureLogin(params: LoginConfig): void {
@@ -20,6 +23,7 @@ export function configureLogin(params: LoginConfig): void {
20
23
  _useInternalAuth = params.useInternalAuth ?? true;
21
24
  _allowRegistration = params.allowRegistration ?? true;
22
25
  _registrationMode = params.registrationMode ?? "open";
26
+ _requireCompanyName = params.requireCompanyName ?? true;
23
27
  }
24
28
 
25
29
  export function isDiscordAuthEnabled(): boolean {
@@ -41,3 +45,7 @@ export function isRegistrationAllowed(): boolean {
41
45
  export function getRegistrationMode(): RegistrationMode {
42
46
  return _registrationMode;
43
47
  }
48
+
49
+ export function isCompanyNameRequired(): boolean {
50
+ return _requireCompanyName;
51
+ }
@@ -1,5 +1,6 @@
1
1
  export {
2
2
  configureLogin,
3
+ isCompanyNameRequired,
3
4
  isDiscordAuthEnabled,
4
5
  isGoogleAuthEnabled,
5
6
  isInternalAuthEnabled,