@carlonicora/nextjs-jsonapi 5.5.0 → 5.6.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-THBJH2UR.js → BlockNoteEditor-AE3ZTEEH.js} +19 -19
  2. package/dist/{BlockNoteEditor-THBJH2UR.js.map → BlockNoteEditor-AE3ZTEEH.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-GVQCKRMU.mjs → BlockNoteEditor-NEYG5L6W.mjs} +4 -4
  4. package/dist/{BlockNoteViewer-U6E3DS4F.mjs → BlockNoteViewer-3OUT5UFY.mjs} +2 -2
  5. package/dist/{BlockNoteViewer-VZOVKTFO.js → BlockNoteViewer-4CFKQM7U.js} +3 -3
  6. package/dist/{BlockNoteViewer-VZOVKTFO.js.map → BlockNoteViewer-4CFKQM7U.js.map} +1 -1
  7. package/dist/billing/index.d.mts +186 -25
  8. package/dist/billing/index.d.ts +186 -25
  9. package/dist/billing/index.js +1148 -587
  10. package/dist/billing/index.js.map +1 -1
  11. package/dist/billing/index.mjs +785 -224
  12. package/dist/billing/index.mjs.map +1 -1
  13. package/dist/{chunk-WPYT4UKJ.js → chunk-3I5AAJXV.js} +3 -3
  14. package/dist/{chunk-WPYT4UKJ.js.map → chunk-3I5AAJXV.js.map} +1 -1
  15. package/dist/{chunk-YDYVJM4I.js → chunk-53BAULDP.js} +4 -4
  16. package/dist/{chunk-YDYVJM4I.js.map → chunk-53BAULDP.js.map} +1 -1
  17. package/dist/{chunk-3CGROPF3.mjs → chunk-BTPUYUQ3.mjs} +2 -2
  18. package/dist/{chunk-VC5A3AUZ.mjs → chunk-EYA2F6AN.mjs} +696 -4
  19. package/dist/chunk-EYA2F6AN.mjs.map +1 -0
  20. package/dist/{chunk-G6UE6MEP.js → chunk-HKZAESS6.js} +896 -204
  21. package/dist/chunk-HKZAESS6.js.map +1 -0
  22. package/dist/{chunk-W7F5BDSV.mjs → chunk-KQHBONL4.mjs} +2 -2
  23. package/dist/{chunk-TY5FYP3G.js → chunk-LQ4DK4JF.js} +1919 -1056
  24. package/dist/chunk-LQ4DK4JF.js.map +1 -0
  25. package/dist/{chunk-S4YJ5WCW.js → chunk-RL3PL3KV.js} +8 -8
  26. package/dist/{chunk-S4YJ5WCW.js.map → chunk-RL3PL3KV.js.map} +1 -1
  27. package/dist/{chunk-X5WIUUNG.mjs → chunk-SFYPXCK4.mjs} +3237 -2374
  28. package/dist/chunk-SFYPXCK4.mjs.map +1 -0
  29. package/dist/{chunk-6SQYXUVP.mjs → chunk-Y7RR57EW.mjs} +2 -2
  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 +242 -3
  37. package/dist/core/index.d.ts +242 -3
  38. package/dist/core/index.js +34 -2
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/index.mjs +33 -1
  41. package/dist/email-automations/index.js +92 -92
  42. package/dist/email-automations/index.mjs +3 -3
  43. package/dist/features/analytics/index.js +121 -121
  44. package/dist/features/analytics/index.mjs +4 -4
  45. package/dist/features/analytics/server.js +4 -4
  46. package/dist/features/analytics/server.mjs +2 -2
  47. package/dist/features/blog/index.js +150 -150
  48. package/dist/features/blog/index.mjs +3 -3
  49. package/dist/features/blog/server.js +16 -16
  50. package/dist/features/blog/server.mjs +1 -1
  51. package/dist/features/help/index.js +37 -37
  52. package/dist/features/help/index.mjs +3 -3
  53. package/dist/features/tokenusage/index.js +124 -124
  54. package/dist/features/tokenusage/index.mjs +4 -4
  55. package/dist/{stripe-promotion-code.interface-C3qqh3mi.d.ts → format-options-BtX3o0Yu.d.ts} +104 -1
  56. package/dist/{stripe-promotion-code.interface-ClZ7DxS9.d.mts → format-options-D6c7FNDi.d.mts} +104 -1
  57. package/dist/index.d.mts +2 -2
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +35 -3
  60. package/dist/index.js.map +1 -1
  61. package/dist/index.mjs +34 -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 +1 -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 +211 -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 +69 -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 +79 -0
  88. package/src/features/billing/stripe-coupon/components/forms/CouponCodeFields.tsx +192 -0
  89. package/src/features/billing/stripe-coupon/components/forms/CouponEditor.tsx +333 -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 +80 -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 +51 -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 +165 -0
  110. package/src/features/billing/stripe-coupon/hooks/useStripeCouponCodeTableStructure.tsx +154 -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 +14 -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 +18 -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 +54 -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-NEYG5L6W.mjs.map} +0 -0
  149. /package/dist/{BlockNoteViewer-U6E3DS4F.mjs.map → BlockNoteViewer-3OUT5UFY.mjs.map} +0 -0
  150. /package/dist/{chunk-3CGROPF3.mjs.map → chunk-BTPUYUQ3.mjs.map} +0 -0
  151. /package/dist/{chunk-W7F5BDSV.mjs.map → chunk-KQHBONL4.mjs.map} +0 -0
  152. /package/dist/{chunk-6SQYXUVP.mjs.map → chunk-Y7RR57EW.mjs.map} +0 -0
@@ -0,0 +1,30 @@
1
+ import { StripeSubscriptionCreateMeta } from "../data/stripe-subscription.interface";
2
+
3
+ /**
4
+ * The client secret the wizard must confirm in the browser after a subscribe
5
+ * or a trial upgrade, or null when the payment needs nothing more.
6
+ *
7
+ * The API creates a subscription with `payment_behavior: "default_incomplete"`:
8
+ * its first PaymentIntent waits on the browser (`requires_confirmation`, or 3D
9
+ * Secure). Until it is confirmed nothing is paid and the subscription stays
10
+ * `incomplete`, so closing the wizard on the API answer alone is wrong.
11
+ */
12
+ export function clientSecretToConfirm(meta: Partial<StripeSubscriptionCreateMeta> | null | undefined): string | null {
13
+ if (!meta?.requiresAction) return null;
14
+ return meta.clientSecret || null;
15
+ }
16
+
17
+ /**
18
+ * The subscription to re-read from Stripe after the browser confirmed its first
19
+ * payment: the one the API just answered with (subscribe), else the one the wizard
20
+ * was opened on (trial upgrade). Null when there is none to sync.
21
+ *
22
+ * Right after the confirmation our copy is still `incomplete` (the webhooks land a
23
+ * second later), so the billing page would show no plan until a reload.
24
+ */
25
+ export function subscriptionIdToSync(
26
+ answered: { id?: string } | null | undefined,
27
+ opened: { id?: string } | null | undefined,
28
+ ): string | null {
29
+ return answered?.id || opened?.id || null;
30
+ }
@@ -0,0 +1,54 @@
1
+ import { PromotionCodeValidationResult } from "../../stripe-promotion-code/data/stripe-promotion-code.interface";
2
+
3
+ /**
4
+ * The promotion code the wizard sends to the API: the one the review shows,
5
+ * typed or automatic. Sending the automatic code too lets the server refuse
6
+ * the payment when that code is no longer valid, instead of charging a price
7
+ * the GM was not shown.
8
+ */
9
+ export function promotionCodeToSend(code: PromotionCodeValidationResult | null): string | undefined {
10
+ if (!code) return undefined;
11
+ return code.promotionCodeId;
12
+ }
13
+
14
+ /**
15
+ * True when the wizard should ask for the automatic code: no code yet, or the
16
+ * current one is automatic. A code the GM typed always wins.
17
+ */
18
+ export function shouldLookUpAutomatic(code: PromotionCodeValidationResult | null): boolean {
19
+ return !code || code.automatic === true;
20
+ }
21
+
22
+ function errorStatus(error: any): number | undefined {
23
+ return error?.status ?? error?.response?.status;
24
+ }
25
+
26
+ /**
27
+ * The `code` of an API error. The nestjs-neo4jsonapi `HttpExceptionFilter`
28
+ * answers with a JSON:API error document, which `AbstractService.callApi`
29
+ * attaches to the thrown error as `body`:
30
+ * `{ message, errors: [{ status, code, title, detail, meta: { context: { code } } }] }`.
31
+ */
32
+ function errorCode(error: any): string | undefined {
33
+ const body = error?.body;
34
+ const first = body?.errors?.[0];
35
+ const code = first?.code ?? first?.meta?.context?.code ?? body?.code;
36
+ return typeof code === "string" ? code : undefined;
37
+ }
38
+
39
+ /**
40
+ * True when the API refused the payment because the automatic discount has no
41
+ * places left (HTTP 410, code `discount_sold_out`). Nothing was charged.
42
+ */
43
+ export function isDiscountSoldOut(error: any): boolean {
44
+ return errorStatus(error) === 410 && errorCode(error) === "discount_sold_out";
45
+ }
46
+
47
+ /**
48
+ * True when the API refused the payment because the automatic code the review
49
+ * showed is no longer valid (HTTP 422, code `discount_unavailable`). Nothing was
50
+ * charged.
51
+ */
52
+ export function isDiscountUnavailable(error: any): boolean {
53
+ return errorStatus(error) === 422 && errorCode(error) === "discount_unavailable";
54
+ }
@@ -225,6 +225,31 @@ export const CurrentUserProvider = ({ children }: { children: React.ReactNode })
225
225
  // Track refresh in progress to prevent duplicate API calls
226
226
  const isRefreshingRef = useRef(false);
227
227
 
228
+ // Revalidate the cached user each time the live connection comes up.
229
+ //
230
+ // The cache above is trusted on mount, and changes reach an open page through
231
+ // `company:subscription_updated`. A change made while no page was connected
232
+ // (the browser closed when a Stripe webhook ended the subscription) never
233
+ // arrives, so the cache kept the old company until a token refresh: the sidebar
234
+ // hid "Upgrade" for a company that no longer had a plan, across reloads. The
235
+ // moment the connection comes up (page load, or back from a network drop) is
236
+ // exactly when such an event may have been missed, so re-read the user once.
237
+ const wasConnectedRef = useRef(false);
238
+ useEffect(() => {
239
+ if (!isConnected) {
240
+ wasConnectedRef.current = false;
241
+ return;
242
+ }
243
+ if (wasConnectedRef.current) return;
244
+ wasConnectedRef.current = true;
245
+
246
+ if (!getCookie("token") || isRefreshingRef.current) return;
247
+ isRefreshingRef.current = true;
248
+ refreshUserRef.current({ skipCookieUpdate: true }).finally(() => {
249
+ isRefreshingRef.current = false;
250
+ });
251
+ }, [isConnected]);
252
+
228
253
  // Listen for company WebSocket events that trigger user refresh
229
254
  useEffect(() => {
230
255
  if (!socket || !isConnected || !currentUser?.company?.id) {
@@ -21,6 +21,8 @@ const permissionsMock = vi.hoisted(() => ({
21
21
  const socketMock = vi.hoisted(() => {
22
22
  const handlers: Record<string, ((data: any) => void)[]> = {};
23
23
  return {
24
+ // Read on every render, so a test can drop and restore the live connection
25
+ isConnected: false,
24
26
  socket: {
25
27
  on: (event: string, handler: (data: any) => void) => {
26
28
  (handlers[event] ??= []).push(handler);
@@ -39,7 +41,7 @@ const socketMock = vi.hoisted(() => {
39
41
  });
40
42
 
41
43
  vi.mock("../../../../contexts/SocketContext", () => ({
42
- useSocketContext: () => ({ socket: socketMock.socket, isConnected: true }),
44
+ useSocketContext: () => ({ socket: socketMock.socket, isConnected: socketMock.isConnected }),
43
45
  }));
44
46
 
45
47
  vi.mock("../../../../core", () => {
@@ -113,6 +115,8 @@ describe("CurrentUserProvider", () => {
113
115
  beforeEach(() => {
114
116
  localStorage.clear();
115
117
  vi.clearAllMocks();
118
+ socketMock.reset();
119
+ socketMock.isConnected = false;
116
120
  });
117
121
 
118
122
  it("keeps user hydrated from localStorage when token cookie is absent", async () => {
@@ -161,7 +165,7 @@ describe("CurrentUserProvider", () => {
161
165
  expect(updateToken).not.toHaveBeenCalled();
162
166
  });
163
167
 
164
- it("does not hit the API when localStorage already holds the user", async () => {
168
+ it("does not hit the API on mount when localStorage already holds the user and no live connection is up", async () => {
165
169
  vi.mocked(getCookie).mockImplementation((name: any) => (name === "token" ? "jwt" : undefined));
166
170
  localStorage.setItem("user", JSON.stringify({ id: "cached", roles: [], company: null, modules: [] }));
167
171
 
@@ -239,6 +243,117 @@ describe("CurrentUserProvider", () => {
239
243
  });
240
244
  });
241
245
 
246
+ describe("CurrentUserProvider live connection revalidation", () => {
247
+ const ACTIVE = { id: "user-1", roles: [], modules: [], company: { id: "company-1", isActiveSubscription: true } };
248
+ const ENDED = { id: "user-1", roles: [], modules: [], company: { id: "company-1", isActiveSubscription: false } };
249
+
250
+ function SubscriptionConsumer() {
251
+ const { company } = useCurrentUserContext();
252
+ return <div data-testid="active">{company ? String((company as any).isActiveSubscription) : "null"}</div>;
253
+ }
254
+
255
+ beforeEach(() => {
256
+ localStorage.clear();
257
+ socketMock.reset();
258
+ vi.clearAllMocks();
259
+ vi.mocked(getCookie).mockImplementation((name: any) => (name === "token" ? "jwt" : undefined));
260
+ vi.mocked(UserService.findFullUser).mockResolvedValue({ ...ENDED, dehydrate: () => ENDED } as any);
261
+ });
262
+
263
+ it("re-reads the user when the live connection comes up, so a change missed while no page was open shows", async () => {
264
+ // The subscription ended (Stripe webhook) while the browser was closed: the cache still says active
265
+ localStorage.setItem("user", JSON.stringify(ACTIVE));
266
+ socketMock.isConnected = true;
267
+
268
+ const { getByTestId } = render(
269
+ <CurrentUserProvider>
270
+ <SubscriptionConsumer />
271
+ </CurrentUserProvider>,
272
+ );
273
+
274
+ await act(async () => {
275
+ await new Promise((resolve) => setTimeout(resolve, 50));
276
+ });
277
+
278
+ expect(UserService.findFullUser).toHaveBeenCalledTimes(1);
279
+ expect(getByTestId("active").textContent).toBe("false");
280
+ });
281
+
282
+ it("re-reads once per connection, not on every render", async () => {
283
+ localStorage.setItem("user", JSON.stringify(ACTIVE));
284
+ socketMock.isConnected = true;
285
+
286
+ const { rerender } = render(
287
+ <CurrentUserProvider>
288
+ <SubscriptionConsumer />
289
+ </CurrentUserProvider>,
290
+ );
291
+ await act(async () => {
292
+ await new Promise((resolve) => setTimeout(resolve, 50));
293
+ });
294
+ rerender(
295
+ <CurrentUserProvider>
296
+ <SubscriptionConsumer />
297
+ </CurrentUserProvider>,
298
+ );
299
+ await act(async () => {
300
+ await new Promise((resolve) => setTimeout(resolve, 50));
301
+ });
302
+
303
+ expect(UserService.findFullUser).toHaveBeenCalledTimes(1);
304
+ });
305
+
306
+ it("re-reads again after the connection drops and comes back", async () => {
307
+ localStorage.setItem("user", JSON.stringify(ACTIVE));
308
+ socketMock.isConnected = true;
309
+
310
+ const tree = (
311
+ <CurrentUserProvider>
312
+ <SubscriptionConsumer />
313
+ </CurrentUserProvider>
314
+ );
315
+ const { rerender } = render(tree);
316
+ await act(async () => {
317
+ await new Promise((resolve) => setTimeout(resolve, 50));
318
+ });
319
+
320
+ socketMock.isConnected = false;
321
+ rerender(
322
+ <CurrentUserProvider>
323
+ <SubscriptionConsumer />
324
+ </CurrentUserProvider>,
325
+ );
326
+ socketMock.isConnected = true;
327
+ rerender(
328
+ <CurrentUserProvider>
329
+ <SubscriptionConsumer />
330
+ </CurrentUserProvider>,
331
+ );
332
+ await act(async () => {
333
+ await new Promise((resolve) => setTimeout(resolve, 50));
334
+ });
335
+
336
+ expect(UserService.findFullUser).toHaveBeenCalledTimes(2);
337
+ });
338
+
339
+ it("does not re-read without a session cookie", async () => {
340
+ vi.mocked(getCookie).mockReturnValue(undefined);
341
+ localStorage.setItem("user", JSON.stringify(ACTIVE));
342
+ socketMock.isConnected = true;
343
+
344
+ render(
345
+ <CurrentUserProvider>
346
+ <SubscriptionConsumer />
347
+ </CurrentUserProvider>,
348
+ );
349
+ await act(async () => {
350
+ await new Promise((resolve) => setTimeout(resolve, 50));
351
+ });
352
+
353
+ expect(UserService.findFullUser).not.toHaveBeenCalled();
354
+ });
355
+ });
356
+
242
357
  describe("CurrentUserProvider token updates", () => {
243
358
  const STORED_USER = {
244
359
  id: "user-1",
@@ -265,6 +380,9 @@ describe("CurrentUserProvider token updates", () => {
265
380
  localStorage.clear();
266
381
  socketMock.reset();
267
382
  vi.clearAllMocks();
383
+ // Connected, with no session cookie: these tests are about socket events, not the reconnect re-read
384
+ socketMock.isConnected = true;
385
+ vi.mocked(getCookie).mockReturnValue(undefined);
268
386
  });
269
387
 
270
388
  it("patches balances from company:credits_updated without refetching the user", async () => {
@@ -352,6 +470,7 @@ describe("CurrentUserProvider path permissions", () => {
352
470
  beforeEach(() => {
353
471
  localStorage.clear();
354
472
  vi.clearAllMocks();
473
+ socketMock.isConnected = false;
355
474
  ctx = undefined;
356
475
  permissionsMock.checkPermissions.mockImplementation(() => true);
357
476
  });