@carlonicora/nextjs-jsonapi 1.44.1 → 1.45.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/README.md +2 -2
  2. package/dist/{BlockNoteEditor-YSOWQEY7.mjs → BlockNoteEditor-IAA6SRJD.mjs} +4 -4
  3. package/dist/{BlockNoteEditor-CFDL7HKM.js → BlockNoteEditor-JYQVZHSR.js} +14 -14
  4. package/dist/{BlockNoteEditor-CFDL7HKM.js.map → BlockNoteEditor-JYQVZHSR.js.map} +1 -1
  5. package/dist/{auth.interface-BJGKQ0zr.d.ts → auth.interface-DgpoGNZN.d.ts} +1 -0
  6. package/dist/{auth.interface-8XglqHir.d.mts → auth.interface-quk7psiq.d.mts} +1 -0
  7. package/dist/billing/index.js +346 -346
  8. package/dist/billing/index.mjs +3 -3
  9. package/dist/{chunk-LBIC4GJK.mjs → chunk-4HMQNMP6.mjs} +28 -2
  10. package/dist/chunk-4HMQNMP6.mjs.map +1 -0
  11. package/dist/{chunk-L5F5ZN5F.js → chunk-AHXRHXZ2.js} +84 -2
  12. package/dist/chunk-AHXRHXZ2.js.map +1 -0
  13. package/dist/{chunk-B456JJWU.mjs → chunk-EA3EPEDL.mjs} +841 -453
  14. package/dist/chunk-EA3EPEDL.mjs.map +1 -0
  15. package/dist/{chunk-OODZEX6P.js → chunk-GP3MDKGE.js} +28 -2
  16. package/dist/chunk-GP3MDKGE.js.map +1 -0
  17. package/dist/{chunk-OXD2QCMX.js → chunk-XRBK4J6U.js} +956 -568
  18. package/dist/chunk-XRBK4J6U.js.map +1 -0
  19. package/dist/{chunk-PHNL4QUF.mjs → chunk-ZMGUP2AI.mjs} +84 -2
  20. package/dist/chunk-ZMGUP2AI.mjs.map +1 -0
  21. package/dist/client/index.js +4 -4
  22. package/dist/client/index.mjs +3 -3
  23. package/dist/components/index.d.mts +160 -3
  24. package/dist/components/index.d.ts +160 -3
  25. package/dist/components/index.js +10 -4
  26. package/dist/components/index.js.map +1 -1
  27. package/dist/components/index.mjs +9 -3
  28. package/dist/contexts/index.js +4 -4
  29. package/dist/contexts/index.mjs +3 -3
  30. package/dist/core/index.d.mts +42 -5
  31. package/dist/core/index.d.ts +42 -5
  32. package/dist/core/index.js +10 -2
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/core/index.mjs +9 -1
  35. package/dist/index.d.mts +66 -5
  36. package/dist/index.d.ts +66 -5
  37. package/dist/index.js +17 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +16 -2
  40. package/dist/{s3.service-DOwqcUDT.d.ts → s3.service--8IFzWsB.d.ts} +1 -1
  41. package/dist/{s3.service-D0rbmLFp.d.mts → s3.service-GQa6F4Ks.d.mts} +1 -1
  42. package/dist/server/index.d.mts +2 -2
  43. package/dist/server/index.d.ts +2 -2
  44. package/dist/server/index.js +3 -3
  45. package/dist/server/index.mjs +1 -1
  46. package/package.json +1 -1
  47. package/scripts/generate-web-module/types/template-data.interface.ts +1 -1
  48. package/src/components/forms/FormSwitch.tsx +0 -1
  49. package/src/components/index.ts +1 -0
  50. package/src/core/index.ts +3 -0
  51. package/src/core/registry/ModuleRegistry.ts +3 -0
  52. package/src/features/auth/components/buttons/GoogleSignInButton.tsx +13 -2
  53. package/src/features/auth/components/forms/Login.tsx +24 -2
  54. package/src/features/auth/components/forms/Logout.tsx +9 -1
  55. package/src/features/auth/components/forms/Register.tsx +45 -5
  56. package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +118 -0
  57. package/src/features/auth/config.ts +1 -1
  58. package/src/features/auth/data/auth.interface.ts +1 -0
  59. package/src/features/auth/data/auth.ts +1 -0
  60. package/src/features/auth/utils/__tests__/clearClientStorage.spec.ts +81 -0
  61. package/src/features/auth/utils/clearClientStorage.ts +11 -0
  62. package/src/features/auth/utils/index.ts +1 -0
  63. package/src/features/index.ts +1 -0
  64. package/src/features/referral/__tests__/config.spec.ts +105 -0
  65. package/src/features/referral/__tests__/referral-cookie.spec.ts +188 -0
  66. package/src/features/referral/components/ReferralCodeCapture.tsx +51 -0
  67. package/src/features/referral/components/ReferralDialog.tsx +94 -0
  68. package/src/features/referral/components/ReferralWidget.tsx +334 -0
  69. package/src/features/referral/components/index.ts +3 -0
  70. package/src/features/referral/config.ts +89 -0
  71. package/src/features/referral/data/ReferralService.ts +38 -0
  72. package/src/features/referral/data/ReferralStats.ts +31 -0
  73. package/src/features/referral/data/index.ts +2 -0
  74. package/src/features/referral/hooks/index.ts +2 -0
  75. package/src/features/referral/hooks/useReferralInvite.ts +32 -0
  76. package/src/features/referral/hooks/useReferralStats.ts +26 -0
  77. package/src/features/referral/index.ts +21 -0
  78. package/src/features/referral/interfaces/index.ts +1 -0
  79. package/src/features/referral/interfaces/referral.interface.ts +5 -0
  80. package/src/features/referral/referral-stats.module.ts +9 -0
  81. package/src/features/referral/referral.module.ts +9 -0
  82. package/src/features/referral/utils/index.ts +1 -0
  83. package/src/features/referral/utils/referral-cookie.ts +35 -0
  84. package/src/features/user/contexts/UserContext.tsx +1 -1
  85. package/src/index.ts +4 -0
  86. package/dist/chunk-B456JJWU.mjs.map +0 -1
  87. package/dist/chunk-L5F5ZN5F.js.map +0 -1
  88. package/dist/chunk-LBIC4GJK.mjs.map +0 -1
  89. package/dist/chunk-OODZEX6P.js.map +0 -1
  90. package/dist/chunk-OXD2QCMX.js.map +0 -1
  91. package/dist/chunk-PHNL4QUF.mjs.map +0 -1
  92. /package/dist/{BlockNoteEditor-YSOWQEY7.mjs.map → BlockNoteEditor-IAA6SRJD.mjs.map} +0 -0
@@ -13,6 +13,7 @@ export type AuthInput = {
13
13
  marketingConsent?: boolean;
14
14
  marketingConsentAt?: string | null;
15
15
  inviteCode?: string;
16
+ referralCode?: string;
16
17
  };
17
18
 
18
19
  export type AuthQuery = {
@@ -50,6 +50,7 @@ export class Auth extends AbstractApiData implements AuthInterface {
50
50
  if (data.marketingConsent !== undefined) response.data.attributes.marketingConsent = data.marketingConsent;
51
51
  if (data.marketingConsentAt !== undefined) response.data.attributes.marketingConsentAt = data.marketingConsentAt;
52
52
  if (data.inviteCode !== undefined) response.data.attributes.inviteCode = data.inviteCode;
53
+ if (data.referralCode !== undefined) response.data.attributes.referralCode = data.referralCode;
53
54
 
54
55
  return response;
55
56
  }
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { clearClientStorage } from "../clearClientStorage";
3
+
4
+ describe("clearClientStorage", () => {
5
+ const originalWindow = global.window;
6
+
7
+ beforeEach(() => {
8
+ // Mock localStorage
9
+ const localStorageMock = {
10
+ removeItem: vi.fn(),
11
+ getItem: vi.fn(),
12
+ setItem: vi.fn(),
13
+ clear: vi.fn(),
14
+ length: 0,
15
+ key: vi.fn(),
16
+ };
17
+ Object.defineProperty(global, "window", {
18
+ value: { localStorage: localStorageMock },
19
+ writable: true,
20
+ });
21
+ });
22
+
23
+ afterEach(() => {
24
+ Object.defineProperty(global, "window", {
25
+ value: originalWindow,
26
+ writable: true,
27
+ });
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ describe("Scenario: Clears specified storage keys", () => {
32
+ it("should remove each specified key from localStorage", () => {
33
+ const keys = ["user", "person", "recentPages"];
34
+
35
+ clearClientStorage(keys);
36
+
37
+ expect(window.localStorage.removeItem).toHaveBeenCalledTimes(3);
38
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith("user");
39
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith("person");
40
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith("recentPages");
41
+ });
42
+ });
43
+
44
+ describe("Scenario: Handles empty key array gracefully", () => {
45
+ it("should not call removeItem when given empty array", () => {
46
+ clearClientStorage([]);
47
+
48
+ expect(window.localStorage.removeItem).not.toHaveBeenCalled();
49
+ });
50
+ });
51
+
52
+ describe("Scenario: SSR-safe execution", () => {
53
+ it("should return early without error when window is undefined", () => {
54
+ Object.defineProperty(global, "window", {
55
+ value: undefined,
56
+ writable: true,
57
+ });
58
+
59
+ expect(() => clearClientStorage(["user", "person"])).not.toThrow();
60
+ });
61
+ });
62
+
63
+ describe("Scenario: Handles keys with special characters", () => {
64
+ it("should properly remove keys containing colons", () => {
65
+ const keys = [
66
+ "react-resizable-panels:page-content-container-desktop:left-panel:right-panel",
67
+ "react-resizable-panels:page-content-container-mobile:left-panel:right-panel",
68
+ ];
69
+
70
+ clearClientStorage(keys);
71
+
72
+ expect(window.localStorage.removeItem).toHaveBeenCalledTimes(2);
73
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(
74
+ "react-resizable-panels:page-content-container-desktop:left-panel:right-panel",
75
+ );
76
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(
77
+ "react-resizable-panels:page-content-container-mobile:left-panel:right-panel",
78
+ );
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Clears specified keys from localStorage during logout.
3
+ * Called before redirect to ensure cleanup happens.
4
+ */
5
+ export function clearClientStorage(keys: string[]): void {
6
+ if (typeof window === "undefined") return;
7
+
8
+ keys.forEach((key) => {
9
+ window.localStorage.removeItem(key);
10
+ });
11
+ }
@@ -1 +1,2 @@
1
1
  export * from "./AuthCookies";
2
+ export * from "./clearClientStorage";
@@ -20,3 +20,4 @@ export * from "./search";
20
20
  export * from "./user";
21
21
  export * from "./oauth";
22
22
  export * from "./waitlist";
23
+ export * from "./referral";
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { configureReferral, getReferralConfig, isReferralEnabled, DEFAULT_REFERRAL_CONFIG } from "../config";
3
+
4
+ describe("Referral Configuration", () => {
5
+ beforeEach(() => {
6
+ // Reset to defaults before each test
7
+ configureReferral({});
8
+ });
9
+
10
+ describe("configureReferral()", () => {
11
+ it("should set config values correctly", () => {
12
+ configureReferral({
13
+ enabled: true,
14
+ cookieName: "my_referral",
15
+ cookieDays: 60,
16
+ urlParamName: "invite",
17
+ referralUrlBase: "https://example.com",
18
+ referralPath: "/signup",
19
+ });
20
+
21
+ const config = getReferralConfig();
22
+ expect(config.enabled).toBe(true);
23
+ expect(config.cookieName).toBe("my_referral");
24
+ expect(config.cookieDays).toBe(60);
25
+ expect(config.urlParamName).toBe("invite");
26
+ expect(config.referralUrlBase).toBe("https://example.com");
27
+ expect(config.referralPath).toBe("/signup");
28
+ });
29
+
30
+ it("should merge custom values with defaults", () => {
31
+ configureReferral({ enabled: true, cookieDays: 60 });
32
+
33
+ const config = getReferralConfig();
34
+ expect(config.enabled).toBe(true);
35
+ expect(config.cookieDays).toBe(60);
36
+ // Default values should be preserved
37
+ expect(config.cookieName).toBe(DEFAULT_REFERRAL_CONFIG.cookieName);
38
+ expect(config.urlParamName).toBe(DEFAULT_REFERRAL_CONFIG.urlParamName);
39
+ expect(config.referralPath).toBe(DEFAULT_REFERRAL_CONFIG.referralPath);
40
+ });
41
+
42
+ it("should overwrite previous configuration (singleton behavior)", () => {
43
+ configureReferral({ enabled: true, cookieDays: 60 });
44
+ configureReferral({ enabled: false, cookieName: "new_cookie" });
45
+
46
+ const config = getReferralConfig();
47
+ expect(config.enabled).toBe(false);
48
+ expect(config.cookieName).toBe("new_cookie");
49
+ // cookieDays should be reset to default
50
+ expect(config.cookieDays).toBe(DEFAULT_REFERRAL_CONFIG.cookieDays);
51
+ });
52
+ });
53
+
54
+ describe("isReferralEnabled()", () => {
55
+ it("should return false by default", () => {
56
+ expect(isReferralEnabled()).toBe(false);
57
+ });
58
+
59
+ it("should return true when enabled", () => {
60
+ configureReferral({ enabled: true });
61
+ expect(isReferralEnabled()).toBe(true);
62
+ });
63
+
64
+ it("should return false when explicitly disabled", () => {
65
+ configureReferral({ enabled: false });
66
+ expect(isReferralEnabled()).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe("getReferralConfig()", () => {
71
+ it("should return default values before configure", () => {
72
+ const config = getReferralConfig();
73
+ expect(config.enabled).toBe(DEFAULT_REFERRAL_CONFIG.enabled);
74
+ expect(config.cookieName).toBe(DEFAULT_REFERRAL_CONFIG.cookieName);
75
+ expect(config.cookieDays).toBe(DEFAULT_REFERRAL_CONFIG.cookieDays);
76
+ expect(config.urlParamName).toBe(DEFAULT_REFERRAL_CONFIG.urlParamName);
77
+ expect(config.referralUrlBase).toBe(DEFAULT_REFERRAL_CONFIG.referralUrlBase);
78
+ expect(config.referralPath).toBe(DEFAULT_REFERRAL_CONFIG.referralPath);
79
+ });
80
+
81
+ it("should return custom values after configure", () => {
82
+ configureReferral({
83
+ enabled: true,
84
+ cookieName: "custom_cookie",
85
+ cookieDays: 45,
86
+ });
87
+
88
+ const config = getReferralConfig();
89
+ expect(config.enabled).toBe(true);
90
+ expect(config.cookieName).toBe("custom_cookie");
91
+ expect(config.cookieDays).toBe(45);
92
+ });
93
+ });
94
+
95
+ describe("DEFAULT_REFERRAL_CONFIG", () => {
96
+ it("should have expected default values", () => {
97
+ expect(DEFAULT_REFERRAL_CONFIG.enabled).toBe(false);
98
+ expect(DEFAULT_REFERRAL_CONFIG.cookieName).toBe("referral_code");
99
+ expect(DEFAULT_REFERRAL_CONFIG.cookieDays).toBe(30);
100
+ expect(DEFAULT_REFERRAL_CONFIG.urlParamName).toBe("ref");
101
+ expect(DEFAULT_REFERRAL_CONFIG.referralUrlBase).toBe("");
102
+ expect(DEFAULT_REFERRAL_CONFIG.referralPath).toBe("/");
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { configureReferral, DEFAULT_REFERRAL_CONFIG } from "../config";
3
+ import { setReferralCode, getReferralCode, clearReferralCode } from "../utils/referral-cookie";
4
+
5
+ describe("Referral Cookie Utilities", () => {
6
+ let cookieStore: Record<string, string> = {};
7
+ let documentCookieDescriptor: PropertyDescriptor | undefined;
8
+
9
+ beforeEach(() => {
10
+ // Reset configuration to defaults
11
+ configureReferral({});
12
+ cookieStore = {};
13
+
14
+ // Save original cookie descriptor
15
+ documentCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, "cookie");
16
+
17
+ // Mock document.cookie
18
+ Object.defineProperty(document, "cookie", {
19
+ get: () => {
20
+ return Object.entries(cookieStore)
21
+ .map(([name, value]) => `${name}=${value}`)
22
+ .join("; ");
23
+ },
24
+ set: (cookieString: string) => {
25
+ // Parse the cookie string
26
+ const parts = cookieString.split(";");
27
+ const [nameValue] = parts;
28
+ const [name, value] = nameValue.split("=");
29
+
30
+ // Check for max-age=0 (deletion)
31
+ const maxAgeMatch = cookieString.match(/max-age=(\d+)/i);
32
+ if (maxAgeMatch && maxAgeMatch[1] === "0") {
33
+ delete cookieStore[name.trim()];
34
+ } else if (value !== undefined) {
35
+ cookieStore[name.trim()] = value;
36
+ }
37
+ },
38
+ configurable: true,
39
+ });
40
+ });
41
+
42
+ afterEach(() => {
43
+ // Restore original cookie descriptor
44
+ if (documentCookieDescriptor) {
45
+ Object.defineProperty(Document.prototype, "cookie", documentCookieDescriptor);
46
+ }
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ describe("setReferralCode()", () => {
51
+ it("should set cookie with configured name", () => {
52
+ setReferralCode("test-code-123");
53
+ expect(cookieStore["referral_code"]).toBe("test-code-123");
54
+ });
55
+
56
+ it("should use custom cookie name when configured", () => {
57
+ configureReferral({ cookieName: "my_ref_code" });
58
+ setReferralCode("custom-code");
59
+ expect(cookieStore["my_ref_code"]).toBe("custom-code");
60
+ });
61
+
62
+ it("should URL encode the referral code", () => {
63
+ setReferralCode("code with spaces & special=chars");
64
+ expect(cookieStore["referral_code"]).toBe("code%20with%20spaces%20%26%20special%3Dchars");
65
+ });
66
+
67
+ it("should set cookie with configured duration in max-age", () => {
68
+ const setCookieSpy = vi.spyOn(document, "cookie", "set");
69
+ configureReferral({ cookieDays: 60 });
70
+
71
+ setReferralCode("test-code");
72
+
73
+ const expectedMaxAge = 60 * 24 * 60 * 60; // 60 days in seconds
74
+ expect(setCookieSpy).toHaveBeenCalledWith(expect.stringContaining(`max-age=${expectedMaxAge}`));
75
+ });
76
+
77
+ it("should use default 30 day duration", () => {
78
+ const setCookieSpy = vi.spyOn(document, "cookie", "set");
79
+ setReferralCode("test-code");
80
+
81
+ const expectedMaxAge = 30 * 24 * 60 * 60; // 30 days in seconds
82
+ expect(setCookieSpy).toHaveBeenCalledWith(expect.stringContaining(`max-age=${expectedMaxAge}`));
83
+ });
84
+
85
+ it("should set path to root and SameSite=Lax", () => {
86
+ const setCookieSpy = vi.spyOn(document, "cookie", "set");
87
+ setReferralCode("test-code");
88
+
89
+ expect(setCookieSpy).toHaveBeenCalledWith(expect.stringContaining("path=/"));
90
+ expect(setCookieSpy).toHaveBeenCalledWith(expect.stringContaining("SameSite=Lax"));
91
+ });
92
+ });
93
+
94
+ describe("getReferralCode()", () => {
95
+ it("should return stored value", () => {
96
+ cookieStore["referral_code"] = "stored-code";
97
+ expect(getReferralCode()).toBe("stored-code");
98
+ });
99
+
100
+ it("should return null when no cookie exists", () => {
101
+ expect(getReferralCode()).toBeNull();
102
+ });
103
+
104
+ it("should use custom cookie name when configured", () => {
105
+ configureReferral({ cookieName: "my_ref_code" });
106
+ cookieStore["my_ref_code"] = "custom-stored-code";
107
+ expect(getReferralCode()).toBe("custom-stored-code");
108
+ });
109
+
110
+ it("should not return value from wrong cookie name", () => {
111
+ configureReferral({ cookieName: "my_ref_code" });
112
+ cookieStore["referral_code"] = "wrong-cookie";
113
+ expect(getReferralCode()).toBeNull();
114
+ });
115
+
116
+ it("should URL decode the referral code", () => {
117
+ cookieStore["referral_code"] = "code%20with%20spaces%20%26%20special%3Dchars";
118
+ expect(getReferralCode()).toBe("code with spaces & special=chars");
119
+ });
120
+
121
+ it("should return null when cookie value is empty", () => {
122
+ cookieStore["referral_code"] = "";
123
+ expect(getReferralCode()).toBeNull();
124
+ });
125
+ });
126
+
127
+ describe("clearReferralCode()", () => {
128
+ it("should remove the cookie", () => {
129
+ cookieStore["referral_code"] = "existing-code";
130
+ clearReferralCode();
131
+ expect(cookieStore["referral_code"]).toBeUndefined();
132
+ });
133
+
134
+ it("should use configured cookie name for clearing", () => {
135
+ configureReferral({ cookieName: "my_ref_code" });
136
+ cookieStore["my_ref_code"] = "existing-code";
137
+ clearReferralCode();
138
+ expect(cookieStore["my_ref_code"]).toBeUndefined();
139
+ });
140
+
141
+ it("should set max-age to 0 for deletion", () => {
142
+ const setCookieSpy = vi.spyOn(document, "cookie", "set");
143
+ clearReferralCode();
144
+ expect(setCookieSpy).toHaveBeenCalledWith(expect.stringContaining("max-age=0"));
145
+ });
146
+
147
+ it("should handle clearing non-existent cookie gracefully", () => {
148
+ // Should not throw
149
+ expect(() => clearReferralCode()).not.toThrow();
150
+ });
151
+ });
152
+
153
+ describe("Integration scenarios", () => {
154
+ it("should set and get referral code correctly", () => {
155
+ setReferralCode("integration-test-code");
156
+ expect(getReferralCode()).toBe("integration-test-code");
157
+ });
158
+
159
+ it("should set, get, and clear referral code correctly", () => {
160
+ setReferralCode("to-be-cleared");
161
+ expect(getReferralCode()).toBe("to-be-cleared");
162
+
163
+ clearReferralCode();
164
+ expect(getReferralCode()).toBeNull();
165
+ });
166
+
167
+ it("should overwrite existing referral code", () => {
168
+ setReferralCode("first-code");
169
+ expect(getReferralCode()).toBe("first-code");
170
+
171
+ setReferralCode("second-code");
172
+ expect(getReferralCode()).toBe("second-code");
173
+ });
174
+
175
+ it("should work with different configurations", () => {
176
+ configureReferral({
177
+ cookieName: "custom_ref",
178
+ cookieDays: 7,
179
+ });
180
+
181
+ setReferralCode("custom-config-code");
182
+ expect(getReferralCode()).toBe("custom-config-code");
183
+
184
+ clearReferralCode();
185
+ expect(getReferralCode()).toBeNull();
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { useSearchParams } from "next/navigation";
4
+ import { useEffect } from "react";
5
+
6
+ import { getReferralConfig, isReferralEnabled } from "../config";
7
+ import { getReferralCode, setReferralCode } from "../utils/referral-cookie";
8
+
9
+ /**
10
+ * ReferralCodeCapture captures referral codes from URL parameters.
11
+ *
12
+ * Behavior:
13
+ * - Checks if referral feature is enabled before processing
14
+ * - Reads the configured query param (default "ref") from the URL
15
+ * - Checks if a referral cookie already exists
16
+ * - If ref param is present AND no existing cookie, sets the cookie (first referrer wins)
17
+ * - Renders nothing (null) - component only captures the code
18
+ */
19
+ export function ReferralCodeCapture(): null {
20
+ const searchParams = useSearchParams();
21
+
22
+ useEffect(() => {
23
+ console.log("[REFERRAL] ReferralCodeCapture mounted");
24
+ console.log("[REFERRAL] isReferralEnabled():", isReferralEnabled());
25
+
26
+ // Skip if feature is disabled
27
+ if (!isReferralEnabled()) {
28
+ console.log("[REFERRAL] Feature DISABLED - not capturing");
29
+ return;
30
+ }
31
+
32
+ const config = getReferralConfig();
33
+ const refCode = searchParams.get(config.urlParamName);
34
+ console.log("[REFERRAL] URL param '" + config.urlParamName + "':", refCode);
35
+
36
+ if (refCode) {
37
+ const existingCode = getReferralCode();
38
+ console.log("[REFERRAL] Existing cookie:", existingCode);
39
+
40
+ // First referrer wins - only set if no existing cookie
41
+ if (!existingCode) {
42
+ setReferralCode(refCode);
43
+ console.log("[REFERRAL] Cookie SET to:", refCode);
44
+ } else {
45
+ console.log("[REFERRAL] Cookie already exists, not overwriting");
46
+ }
47
+ }
48
+ }, [searchParams]);
49
+
50
+ return null;
51
+ }
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../../../shadcnui";
4
+ import { isReferralEnabled } from "../config";
5
+ import { ReferralWidget, ReferralWidgetProps, ReferralWidgetTranslations } from "./ReferralWidget";
6
+
7
+ /**
8
+ * Translation strings for the ReferralDialog component.
9
+ * Extends ReferralWidgetTranslations with dialog-specific strings.
10
+ */
11
+ export interface ReferralDialogTranslations extends ReferralWidgetTranslations {
12
+ /** Dialog title */
13
+ dialogTitle?: string;
14
+ /** Dialog description */
15
+ dialogDescription?: string;
16
+ }
17
+
18
+ /**
19
+ * Default translations for ReferralDialog.
20
+ */
21
+ const DEFAULT_DIALOG_TRANSLATIONS: Required<Pick<ReferralDialogTranslations, "dialogTitle" | "dialogDescription">> = {
22
+ dialogTitle: "Invite Friends",
23
+ dialogDescription: "Share your referral link and earn rewards when your friends subscribe.",
24
+ };
25
+
26
+ /**
27
+ * Props for the ReferralDialog component.
28
+ */
29
+ export interface ReferralDialogProps extends Omit<ReferralWidgetProps, "isDialog" | "translations"> {
30
+ /** Whether the dialog is open */
31
+ open: boolean;
32
+ /** Callback when the dialog open state changes */
33
+ onOpenChange: (open: boolean) => void;
34
+ /** Custom translations to override defaults */
35
+ translations?: ReferralDialogTranslations;
36
+ /** Additional CSS class name for the dialog content */
37
+ dialogClassName?: string;
38
+ }
39
+
40
+ /**
41
+ * ReferralDialog displays the ReferralWidget in a modal dialog.
42
+ *
43
+ * Features:
44
+ * - Feature flag awareness (renders null when disabled)
45
+ * - Uses shadcn Dialog components
46
+ * - Passes through all ReferralWidget props
47
+ * - Customizable translations including dialog-specific text
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * import { ReferralDialog } from "@carlonicora/nextjs-jsonapi/components";
52
+ *
53
+ * // Basic usage
54
+ * const [open, setOpen] = useState(false);
55
+ * <ReferralDialog open={open} onOpenChange={setOpen} />
56
+ *
57
+ * // With custom translations
58
+ * <ReferralDialog
59
+ * open={open}
60
+ * onOpenChange={setOpen}
61
+ * translations={{
62
+ * dialogTitle: t("referral.title"),
63
+ * dialogDescription: t("referral.description"),
64
+ * yourLink: t("referral.yourLink"),
65
+ * }}
66
+ * />
67
+ * ```
68
+ */
69
+ export function ReferralDialog({
70
+ open,
71
+ onOpenChange,
72
+ translations,
73
+ dialogClassName,
74
+ ...widgetProps
75
+ }: ReferralDialogProps) {
76
+ // Render nothing when disabled
77
+ if (!isReferralEnabled()) {
78
+ return null;
79
+ }
80
+
81
+ const t = { ...DEFAULT_DIALOG_TRANSLATIONS, ...translations };
82
+
83
+ return (
84
+ <Dialog open={open} onOpenChange={onOpenChange}>
85
+ <DialogContent className={dialogClassName ?? "max-w-md"}>
86
+ <DialogHeader>
87
+ <DialogTitle>{t.dialogTitle}</DialogTitle>
88
+ <DialogDescription>{t.dialogDescription}</DialogDescription>
89
+ </DialogHeader>
90
+ <ReferralWidget {...widgetProps} translations={translations} isDialog />
91
+ </DialogContent>
92
+ </Dialog>
93
+ );
94
+ }