@solvapay/react 1.0.8-preview.6 → 1.0.8-preview.8

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 (106) hide show
  1. package/README.md +12 -95
  2. package/dist/CancelPlanButton-Bb7cvym9.d.cts +1105 -0
  3. package/dist/CancelPlanButton-CieT9swn.d.cts +1102 -0
  4. package/dist/CancelPlanButton-Cq117t7h.d.ts +1105 -0
  5. package/dist/CancelPlanButton-f56UlQN-.d.ts +1102 -0
  6. package/dist/PaymentForm-B5dw0i1X.d.ts +934 -0
  7. package/dist/PaymentForm-CesoRQWu.d.cts +934 -0
  8. package/dist/chunk-C7SKOENW.js +1889 -0
  9. package/dist/chunk-ISYAH4ZL.js +1889 -0
  10. package/dist/chunk-MOP3ZBGC.js +4749 -0
  11. package/dist/chunk-Q7GPX6X4.js +4748 -0
  12. package/dist/chunk-TP4JPKE2.js +3126 -0
  13. package/dist/chunk-VTGIXJQU.js +4755 -0
  14. package/dist/chunk-XMQ4GBWQ.js +4752 -0
  15. package/dist/index-DLl3UwO3.d.ts +798 -0
  16. package/dist/index-Sj2lHLl8.d.cts +798 -0
  17. package/dist/index.cjs +11 -25
  18. package/dist/index.d.cts +2 -2
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +1 -1
  21. package/dist/primitives/index.cjs +4 -21
  22. package/dist/primitives/index.d.cts +2 -2
  23. package/dist/primitives/index.d.ts +2 -2
  24. package/dist/primitives/index.js +1 -1
  25. package/dist/src/PaymentForm.d.ts +56 -0
  26. package/dist/src/PaymentForm.d.ts.map +1 -0
  27. package/dist/src/PaymentForm.js +212 -0
  28. package/dist/src/PaymentForm.js.map +1 -0
  29. package/dist/src/SolvaPayProvider.d.ts +84 -0
  30. package/dist/src/SolvaPayProvider.d.ts.map +1 -0
  31. package/dist/src/SolvaPayProvider.js +537 -0
  32. package/dist/src/SolvaPayProvider.js.map +1 -0
  33. package/dist/src/adapters/auth.d.ts +43 -0
  34. package/dist/src/adapters/auth.d.ts.map +1 -0
  35. package/dist/src/adapters/auth.js +38 -0
  36. package/dist/src/adapters/auth.js.map +1 -0
  37. package/dist/src/components/PlanBadge.d.ts +31 -0
  38. package/dist/src/components/PlanBadge.d.ts.map +1 -0
  39. package/dist/src/components/PlanBadge.js +119 -0
  40. package/dist/src/components/PlanBadge.js.map +1 -0
  41. package/dist/src/components/PlanSelector.d.ts +49 -0
  42. package/dist/src/components/PlanSelector.d.ts.map +1 -0
  43. package/dist/src/components/PlanSelector.js +82 -0
  44. package/dist/src/components/PlanSelector.js.map +1 -0
  45. package/dist/src/components/Spinner.d.ts +10 -0
  46. package/dist/src/components/Spinner.d.ts.map +1 -0
  47. package/dist/src/components/Spinner.js +14 -0
  48. package/dist/src/components/Spinner.js.map +1 -0
  49. package/dist/src/components/StripePaymentFormWrapper.d.ts +17 -0
  50. package/dist/src/components/StripePaymentFormWrapper.d.ts.map +1 -0
  51. package/dist/src/components/StripePaymentFormWrapper.js +158 -0
  52. package/dist/src/components/StripePaymentFormWrapper.js.map +1 -0
  53. package/dist/src/components/SubscriptionGate.d.ts +21 -0
  54. package/dist/src/components/SubscriptionGate.d.ts.map +1 -0
  55. package/dist/src/components/SubscriptionGate.js +32 -0
  56. package/dist/src/components/SubscriptionGate.js.map +1 -0
  57. package/dist/src/hooks/__tests__/useSubscription.test.d.ts +2 -0
  58. package/dist/src/hooks/__tests__/useSubscription.test.d.ts.map +1 -0
  59. package/dist/src/hooks/__tests__/useSubscription.test.js +554 -0
  60. package/dist/src/hooks/__tests__/useSubscription.test.js.map +1 -0
  61. package/dist/src/hooks/__tests__/useSubscriptionStatus.test.d.ts +2 -0
  62. package/dist/src/hooks/__tests__/useSubscriptionStatus.test.d.ts.map +1 -0
  63. package/dist/src/hooks/__tests__/useSubscriptionStatus.test.js +719 -0
  64. package/dist/src/hooks/__tests__/useSubscriptionStatus.test.js.map +1 -0
  65. package/dist/src/hooks/useCheckout.d.ts +59 -0
  66. package/dist/src/hooks/useCheckout.d.ts.map +1 -0
  67. package/dist/src/hooks/useCheckout.js +138 -0
  68. package/dist/src/hooks/useCheckout.js.map +1 -0
  69. package/dist/src/hooks/useCustomer.d.ts +43 -0
  70. package/dist/src/hooks/useCustomer.d.ts.map +1 -0
  71. package/dist/src/hooks/useCustomer.js +31 -0
  72. package/dist/src/hooks/useCustomer.js.map +1 -0
  73. package/dist/src/hooks/usePlans.d.ts +28 -0
  74. package/dist/src/hooks/usePlans.d.ts.map +1 -0
  75. package/dist/src/hooks/usePlans.js +181 -0
  76. package/dist/src/hooks/usePlans.js.map +1 -0
  77. package/dist/src/hooks/useSolvaPay.d.ts +44 -0
  78. package/dist/src/hooks/useSolvaPay.d.ts.map +1 -0
  79. package/dist/src/hooks/useSolvaPay.js +52 -0
  80. package/dist/src/hooks/useSolvaPay.js.map +1 -0
  81. package/dist/src/hooks/useSubscription.d.ts +46 -0
  82. package/dist/src/hooks/useSubscription.d.ts.map +1 -0
  83. package/dist/src/hooks/useSubscription.js +50 -0
  84. package/dist/src/hooks/useSubscription.js.map +1 -0
  85. package/dist/src/hooks/useSubscriptionStatus.d.ts +19 -0
  86. package/dist/src/hooks/useSubscriptionStatus.d.ts.map +1 -0
  87. package/dist/src/hooks/useSubscriptionStatus.js +68 -0
  88. package/dist/src/hooks/useSubscriptionStatus.js.map +1 -0
  89. package/dist/src/index.d.ts +24 -0
  90. package/dist/src/index.d.ts.map +1 -0
  91. package/dist/src/index.js +26 -0
  92. package/dist/src/index.js.map +1 -0
  93. package/dist/src/types/index.d.ts +339 -0
  94. package/dist/src/types/index.d.ts.map +1 -0
  95. package/dist/src/types/index.js +5 -0
  96. package/dist/src/types/index.js.map +1 -0
  97. package/dist/src/utils/__tests__/subscriptions.test.d.ts +2 -0
  98. package/dist/src/utils/__tests__/subscriptions.test.d.ts.map +1 -0
  99. package/dist/src/utils/__tests__/subscriptions.test.js +121 -0
  100. package/dist/src/utils/__tests__/subscriptions.test.js.map +1 -0
  101. package/dist/src/utils/subscriptions.d.ts +57 -0
  102. package/dist/src/utils/subscriptions.d.ts.map +1 -0
  103. package/dist/src/utils/subscriptions.js +85 -0
  104. package/dist/src/utils/subscriptions.js.map +1 -0
  105. package/dist/tsconfig.tsbuildinfo +1 -0
  106. package/package.json +2 -2
@@ -0,0 +1,719 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useSubscriptionStatus } from '../useSubscriptionStatus';
4
+ import * as useSubscriptionModule from '../useSubscription';
5
+ // Helper function to create a test subscription
6
+ const createSubscription = (overrides = {}) => ({
7
+ reference: 'sub_123',
8
+ planName: 'Test Plan',
9
+ agentName: 'Test Agent',
10
+ status: 'active',
11
+ startDate: '2024-01-01T00:00:00Z',
12
+ amount: 1000,
13
+ ...overrides,
14
+ });
15
+ // Mock useSubscription
16
+ vi.mock('../useSubscription', () => ({
17
+ useSubscription: vi.fn(),
18
+ }));
19
+ describe('useSubscriptionStatus', () => {
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ });
23
+ describe('Basic functionality', () => {
24
+ it('should return all expected properties', () => {
25
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
26
+ subscriptions: [],
27
+ loading: false,
28
+ hasPlan: vi.fn(),
29
+ activeSubscription: null,
30
+ hasPaidSubscription: false,
31
+ activePaidSubscription: null,
32
+ refetch: vi.fn(),
33
+ });
34
+ const { result } = renderHook(() => useSubscriptionStatus());
35
+ expect(result.current).toHaveProperty('cancelledSubscription');
36
+ expect(result.current).toHaveProperty('shouldShowCancelledNotice');
37
+ expect(result.current).toHaveProperty('formatDate');
38
+ expect(result.current).toHaveProperty('getDaysUntilExpiration');
39
+ });
40
+ it('should use subscriptions from useSubscription hook', () => {
41
+ const subscriptions = [createSubscription()];
42
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
43
+ subscriptions,
44
+ loading: false,
45
+ hasPlan: vi.fn(),
46
+ activeSubscription: null,
47
+ hasPaidSubscription: false,
48
+ activePaidSubscription: null,
49
+ refetch: vi.fn(),
50
+ });
51
+ const { result } = renderHook(() => useSubscriptionStatus());
52
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
53
+ expect(result.current.cancelledSubscription).toBeNull();
54
+ });
55
+ });
56
+ describe('cancelledSubscription', () => {
57
+ it('should return null when no cancelled subscriptions exist', () => {
58
+ const subscriptions = [
59
+ createSubscription({ status: 'active', amount: 1000 }),
60
+ createSubscription({ status: 'active', amount: 2000 }),
61
+ ];
62
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
63
+ subscriptions,
64
+ loading: false,
65
+ hasPlan: vi.fn(),
66
+ activeSubscription: null,
67
+ hasPaidSubscription: true,
68
+ activePaidSubscription: subscriptions[0],
69
+ refetch: vi.fn(),
70
+ });
71
+ const { result } = renderHook(() => useSubscriptionStatus());
72
+ expect(result.current.cancelledSubscription).toBeNull();
73
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
74
+ });
75
+ it('should return null when cancelled subscription is free (amount === 0)', () => {
76
+ const subscriptions = [
77
+ createSubscription({ status: 'active', amount: 0, cancelledAt: '2024-06-01T00:00:00Z' }),
78
+ ];
79
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
80
+ subscriptions,
81
+ loading: false,
82
+ hasPlan: vi.fn(),
83
+ activeSubscription: null,
84
+ hasPaidSubscription: false,
85
+ activePaidSubscription: null,
86
+ refetch: vi.fn(),
87
+ });
88
+ const { result } = renderHook(() => useSubscriptionStatus());
89
+ expect(result.current.cancelledSubscription).toBeNull();
90
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
91
+ });
92
+ it('should return null when cancelled subscription has undefined amount', () => {
93
+ const subscriptions = [
94
+ createSubscription({
95
+ status: 'active',
96
+ amount: undefined,
97
+ cancelledAt: '2024-06-01T00:00:00Z',
98
+ }),
99
+ ];
100
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
101
+ subscriptions,
102
+ loading: false,
103
+ hasPlan: vi.fn(),
104
+ activeSubscription: null,
105
+ hasPaidSubscription: false,
106
+ activePaidSubscription: null,
107
+ refetch: vi.fn(),
108
+ });
109
+ const { result } = renderHook(() => useSubscriptionStatus());
110
+ expect(result.current.cancelledSubscription).toBeNull();
111
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
112
+ });
113
+ it('should return cancelled paid subscription when one exists', () => {
114
+ const cancelledSub = createSubscription({
115
+ status: 'active',
116
+ amount: 1000,
117
+ planName: 'Cancelled Plan',
118
+ reference: 'sub_cancelled',
119
+ cancelledAt: '2024-06-01T00:00:00Z',
120
+ });
121
+ const subscriptions = [cancelledSub];
122
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
123
+ subscriptions,
124
+ loading: false,
125
+ hasPlan: vi.fn(),
126
+ activeSubscription: null,
127
+ hasPaidSubscription: false,
128
+ activePaidSubscription: null,
129
+ refetch: vi.fn(),
130
+ });
131
+ const { result } = renderHook(() => useSubscriptionStatus());
132
+ expect(result.current.cancelledSubscription).not.toBeNull();
133
+ expect(result.current.cancelledSubscription?.planName).toBe('Cancelled Plan');
134
+ expect(result.current.cancelledSubscription?.status).toBe('active');
135
+ expect(result.current.cancelledSubscription?.amount).toBe(1000);
136
+ expect(result.current.shouldShowCancelledNotice).toBe(true);
137
+ });
138
+ it('should return most recent cancelled subscription when multiple exist', () => {
139
+ const olderCancelled = createSubscription({
140
+ status: 'active',
141
+ amount: 1000,
142
+ cancelledAt: '2024-06-01T00:00:00Z',
143
+ planName: 'Older Plan',
144
+ startDate: '2024-01-01T00:00:00Z',
145
+ });
146
+ const newerCancelled = createSubscription({
147
+ status: 'active',
148
+ cancelledAt: '2024-06-01T00:00:00Z',
149
+ amount: 2000,
150
+ planName: 'Newer Plan',
151
+ startDate: '2024-02-01T00:00:00Z',
152
+ });
153
+ const subscriptions = [olderCancelled, newerCancelled];
154
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
155
+ subscriptions,
156
+ loading: false,
157
+ hasPlan: vi.fn(),
158
+ activeSubscription: null,
159
+ hasPaidSubscription: false,
160
+ activePaidSubscription: null,
161
+ refetch: vi.fn(),
162
+ });
163
+ const { result } = renderHook(() => useSubscriptionStatus());
164
+ expect(result.current.cancelledSubscription).not.toBeNull();
165
+ expect(result.current.cancelledSubscription?.planName).toBe('Newer Plan');
166
+ expect(result.current.cancelledSubscription?.startDate).toBe('2024-02-01T00:00:00Z');
167
+ });
168
+ it('should only consider cancelled paid subscriptions', () => {
169
+ const cancelledPaid = createSubscription({
170
+ status: 'active',
171
+ cancelledAt: '2024-06-01T00:00:00Z',
172
+ amount: 1000,
173
+ planName: 'Cancelled Paid',
174
+ });
175
+ const cancelledFree = createSubscription({
176
+ status: 'active',
177
+ cancelledAt: '2024-06-01T00:00:00Z',
178
+ amount: 0,
179
+ planName: 'Cancelled Free',
180
+ });
181
+ const activePaid = createSubscription({
182
+ status: 'active',
183
+ amount: 2000,
184
+ planName: 'Active Paid',
185
+ });
186
+ const subscriptions = [cancelledPaid, cancelledFree, activePaid];
187
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
188
+ subscriptions,
189
+ loading: false,
190
+ hasPlan: vi.fn(),
191
+ activeSubscription: activePaid,
192
+ hasPaidSubscription: true,
193
+ activePaidSubscription: activePaid,
194
+ refetch: vi.fn(),
195
+ });
196
+ const { result } = renderHook(() => useSubscriptionStatus());
197
+ expect(result.current.cancelledSubscription).not.toBeNull();
198
+ expect(result.current.cancelledSubscription?.planName).toBe('Cancelled Paid');
199
+ expect(result.current.cancelledSubscription?.amount).toBe(1000);
200
+ });
201
+ it('should handle empty subscriptions array', () => {
202
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
203
+ subscriptions: [],
204
+ loading: false,
205
+ hasPlan: vi.fn(),
206
+ activeSubscription: null,
207
+ hasPaidSubscription: false,
208
+ activePaidSubscription: null,
209
+ refetch: vi.fn(),
210
+ });
211
+ const { result } = renderHook(() => useSubscriptionStatus());
212
+ expect(result.current.cancelledSubscription).toBeNull();
213
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
214
+ });
215
+ });
216
+ describe('shouldShowCancelledNotice', () => {
217
+ it('should be false when no cancelled subscription exists', () => {
218
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
219
+ subscriptions: [],
220
+ loading: false,
221
+ hasPlan: vi.fn(),
222
+ activeSubscription: null,
223
+ hasPaidSubscription: false,
224
+ activePaidSubscription: null,
225
+ refetch: vi.fn(),
226
+ });
227
+ const { result } = renderHook(() => useSubscriptionStatus());
228
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
229
+ });
230
+ it('should be true when cancelled subscription exists', () => {
231
+ const cancelledSub = createSubscription({
232
+ status: 'active',
233
+ cancelledAt: '2024-06-01T00:00:00Z',
234
+ amount: 1000,
235
+ });
236
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
237
+ subscriptions: [cancelledSub],
238
+ loading: false,
239
+ hasPlan: vi.fn(),
240
+ activeSubscription: null,
241
+ hasPaidSubscription: false,
242
+ activePaidSubscription: null,
243
+ refetch: vi.fn(),
244
+ });
245
+ const { result } = renderHook(() => useSubscriptionStatus());
246
+ expect(result.current.shouldShowCancelledNotice).toBe(true);
247
+ });
248
+ it('should be false when only cancelled free subscription exists', () => {
249
+ const cancelledFree = createSubscription({
250
+ status: 'active',
251
+ cancelledAt: '2024-06-01T00:00:00Z',
252
+ amount: 0,
253
+ });
254
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
255
+ subscriptions: [cancelledFree],
256
+ loading: false,
257
+ hasPlan: vi.fn(),
258
+ activeSubscription: null,
259
+ hasPaidSubscription: false,
260
+ activePaidSubscription: null,
261
+ refetch: vi.fn(),
262
+ });
263
+ const { result } = renderHook(() => useSubscriptionStatus());
264
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
265
+ });
266
+ });
267
+ describe('formatDate', () => {
268
+ it('should format valid date string', () => {
269
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
270
+ subscriptions: [],
271
+ loading: false,
272
+ hasPlan: vi.fn(),
273
+ activeSubscription: null,
274
+ hasPaidSubscription: false,
275
+ activePaidSubscription: null,
276
+ refetch: vi.fn(),
277
+ });
278
+ const { result } = renderHook(() => useSubscriptionStatus());
279
+ const formatted = result.current.formatDate('2024-01-15T00:00:00Z');
280
+ expect(formatted).toBeTruthy();
281
+ expect(typeof formatted).toBe('string');
282
+ // Should contain month name, day, and year
283
+ expect(formatted).toMatch(/January|February|March|April|May|June|July|August|September|October|November|December/);
284
+ expect(formatted).toMatch(/2024/);
285
+ });
286
+ it('should return null for undefined date', () => {
287
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
288
+ subscriptions: [],
289
+ loading: false,
290
+ hasPlan: vi.fn(),
291
+ activeSubscription: null,
292
+ hasPaidSubscription: false,
293
+ activePaidSubscription: null,
294
+ refetch: vi.fn(),
295
+ });
296
+ const { result } = renderHook(() => useSubscriptionStatus());
297
+ expect(result.current.formatDate(undefined)).toBeNull();
298
+ });
299
+ it('should return null for empty string', () => {
300
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
301
+ subscriptions: [],
302
+ loading: false,
303
+ hasPlan: vi.fn(),
304
+ activeSubscription: null,
305
+ hasPaidSubscription: false,
306
+ activePaidSubscription: null,
307
+ refetch: vi.fn(),
308
+ });
309
+ const { result } = renderHook(() => useSubscriptionStatus());
310
+ // Empty string is falsy, so should be treated as undefined and return null
311
+ const formatted = result.current.formatDate('');
312
+ expect(formatted).toBeNull();
313
+ });
314
+ it('should format different dates correctly', () => {
315
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
316
+ subscriptions: [],
317
+ loading: false,
318
+ hasPlan: vi.fn(),
319
+ activeSubscription: null,
320
+ hasPaidSubscription: false,
321
+ activePaidSubscription: null,
322
+ refetch: vi.fn(),
323
+ });
324
+ const { result } = renderHook(() => useSubscriptionStatus());
325
+ const date1 = result.current.formatDate('2024-12-31T23:59:59Z');
326
+ const date2 = result.current.formatDate('2024-01-01T00:00:00Z');
327
+ expect(date1).toBeTruthy();
328
+ expect(date2).toBeTruthy();
329
+ expect(date1).not.toBe(date2);
330
+ });
331
+ it('should use en-US locale format', () => {
332
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
333
+ subscriptions: [],
334
+ loading: false,
335
+ hasPlan: vi.fn(),
336
+ activeSubscription: null,
337
+ hasPaidSubscription: false,
338
+ activePaidSubscription: null,
339
+ refetch: vi.fn(),
340
+ });
341
+ const { result } = renderHook(() => useSubscriptionStatus());
342
+ const formatted = result.current.formatDate('2024-03-15T00:00:00Z');
343
+ // Should be in format like "March 15, 2024"
344
+ expect(formatted).toContain('March');
345
+ expect(formatted).toContain('15');
346
+ expect(formatted).toContain('2024');
347
+ });
348
+ });
349
+ describe('getDaysUntilExpiration', () => {
350
+ it('should return null for undefined endDate', () => {
351
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
352
+ subscriptions: [],
353
+ loading: false,
354
+ hasPlan: vi.fn(),
355
+ activeSubscription: null,
356
+ hasPaidSubscription: false,
357
+ activePaidSubscription: null,
358
+ refetch: vi.fn(),
359
+ });
360
+ const { result } = renderHook(() => useSubscriptionStatus());
361
+ expect(result.current.getDaysUntilExpiration(undefined)).toBeNull();
362
+ });
363
+ it('should return null for empty string endDate', () => {
364
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
365
+ subscriptions: [],
366
+ loading: false,
367
+ hasPlan: vi.fn(),
368
+ activeSubscription: null,
369
+ hasPaidSubscription: false,
370
+ activePaidSubscription: null,
371
+ refetch: vi.fn(),
372
+ });
373
+ const { result } = renderHook(() => useSubscriptionStatus());
374
+ // Empty string is falsy, so should return null
375
+ const resultValue = result.current.getDaysUntilExpiration('');
376
+ expect(resultValue).toBeNull();
377
+ });
378
+ it('should return positive days for future date', () => {
379
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
380
+ subscriptions: [],
381
+ loading: false,
382
+ hasPlan: vi.fn(),
383
+ activeSubscription: null,
384
+ hasPaidSubscription: false,
385
+ activePaidSubscription: null,
386
+ refetch: vi.fn(),
387
+ });
388
+ const { result } = renderHook(() => useSubscriptionStatus());
389
+ // Create a date 10 days in the future
390
+ const futureDate = new Date();
391
+ futureDate.setDate(futureDate.getDate() + 10);
392
+ const futureDateString = futureDate.toISOString();
393
+ const days = result.current.getDaysUntilExpiration(futureDateString);
394
+ expect(days).toBeGreaterThan(0);
395
+ expect(days).toBeLessThanOrEqual(10); // Should be around 10, might be 9 or 10 depending on time of day
396
+ });
397
+ it('should return 0 for past date', () => {
398
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
399
+ subscriptions: [],
400
+ loading: false,
401
+ hasPlan: vi.fn(),
402
+ activeSubscription: null,
403
+ hasPaidSubscription: false,
404
+ activePaidSubscription: null,
405
+ refetch: vi.fn(),
406
+ });
407
+ const { result } = renderHook(() => useSubscriptionStatus());
408
+ const pastDate = new Date('2020-01-01T00:00:00Z');
409
+ const days = result.current.getDaysUntilExpiration(pastDate.toISOString());
410
+ expect(days).toBe(0);
411
+ });
412
+ it('should return 0 for date exactly today', () => {
413
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
414
+ subscriptions: [],
415
+ loading: false,
416
+ hasPlan: vi.fn(),
417
+ activeSubscription: null,
418
+ hasPaidSubscription: false,
419
+ activePaidSubscription: null,
420
+ refetch: vi.fn(),
421
+ });
422
+ const { result } = renderHook(() => useSubscriptionStatus());
423
+ const today = new Date();
424
+ today.setHours(23, 59, 59, 999); // End of today
425
+ const days = result.current.getDaysUntilExpiration(today.toISOString());
426
+ // Should be 0 or 1 depending on when test runs
427
+ expect(days).toBeGreaterThanOrEqual(0);
428
+ expect(days).toBeLessThanOrEqual(1);
429
+ });
430
+ it('should calculate days correctly for various future dates', () => {
431
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
432
+ subscriptions: [],
433
+ loading: false,
434
+ hasPlan: vi.fn(),
435
+ activeSubscription: null,
436
+ hasPaidSubscription: false,
437
+ activePaidSubscription: null,
438
+ refetch: vi.fn(),
439
+ });
440
+ const { result } = renderHook(() => useSubscriptionStatus());
441
+ const now = new Date();
442
+ const date1 = new Date(now);
443
+ date1.setDate(date1.getDate() + 1);
444
+ const date2 = new Date(now);
445
+ date2.setDate(date2.getDate() + 30);
446
+ const date3 = new Date(now);
447
+ date3.setDate(date3.getDate() + 365);
448
+ const days1 = result.current.getDaysUntilExpiration(date1.toISOString());
449
+ const days2 = result.current.getDaysUntilExpiration(date2.toISOString());
450
+ const days3 = result.current.getDaysUntilExpiration(date3.toISOString());
451
+ expect(days1).toBeGreaterThan(0);
452
+ expect(days2).toBeGreaterThan(25); // Should be around 30
453
+ expect(days3).toBeGreaterThan(360); // Should be around 365
454
+ expect(days1).not.toBeNull();
455
+ expect(days2).not.toBeNull();
456
+ expect(days3).not.toBeNull();
457
+ expect(days1).toBeLessThan(days2);
458
+ expect(days2).toBeLessThan(days3);
459
+ });
460
+ it('should use Math.ceil for rounding', () => {
461
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
462
+ subscriptions: [],
463
+ loading: false,
464
+ hasPlan: vi.fn(),
465
+ activeSubscription: null,
466
+ hasPaidSubscription: false,
467
+ activePaidSubscription: null,
468
+ refetch: vi.fn(),
469
+ });
470
+ const { result } = renderHook(() => useSubscriptionStatus());
471
+ // Create a date 1.5 days in the future (should round up to 2)
472
+ const futureDate = new Date();
473
+ futureDate.setTime(futureDate.getTime() + 1.5 * 24 * 60 * 60 * 1000);
474
+ const days = result.current.getDaysUntilExpiration(futureDate.toISOString());
475
+ expect(days).toBeGreaterThanOrEqual(1);
476
+ expect(days).toBeLessThanOrEqual(2);
477
+ });
478
+ });
479
+ describe('Edge cases and integration', () => {
480
+ it('should handle subscription with negative amount', () => {
481
+ const subscription = createSubscription({
482
+ status: 'active',
483
+ cancelledAt: '2024-06-01T00:00:00Z',
484
+ amount: -100,
485
+ });
486
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
487
+ subscriptions: [subscription],
488
+ loading: false,
489
+ hasPlan: vi.fn(),
490
+ activeSubscription: null,
491
+ hasPaidSubscription: false,
492
+ activePaidSubscription: null,
493
+ refetch: vi.fn(),
494
+ });
495
+ const { result } = renderHook(() => useSubscriptionStatus());
496
+ // Negative amount should not be considered paid
497
+ expect(result.current.cancelledSubscription).toBeNull();
498
+ expect(result.current.shouldShowCancelledNotice).toBe(false);
499
+ });
500
+ it('should handle mixed subscription states', () => {
501
+ const activePaid = createSubscription({
502
+ status: 'active',
503
+ amount: 2000,
504
+ planName: 'Active Paid',
505
+ });
506
+ const cancelledPaid = createSubscription({
507
+ status: 'active',
508
+ cancelledAt: '2024-06-01T00:00:00Z',
509
+ amount: 1000,
510
+ planName: 'Cancelled Paid',
511
+ startDate: '2024-01-01T00:00:00Z',
512
+ });
513
+ const activeFree = createSubscription({
514
+ status: 'active',
515
+ amount: 0,
516
+ planName: 'Active Free',
517
+ });
518
+ const subscriptions = [activePaid, cancelledPaid, activeFree];
519
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
520
+ subscriptions,
521
+ loading: false,
522
+ hasPlan: vi.fn(),
523
+ activeSubscription: activePaid,
524
+ hasPaidSubscription: true,
525
+ activePaidSubscription: activePaid,
526
+ refetch: vi.fn(),
527
+ });
528
+ const { result } = renderHook(() => useSubscriptionStatus());
529
+ expect(result.current.cancelledSubscription).not.toBeNull();
530
+ expect(result.current.cancelledSubscription?.planName).toBe('Cancelled Paid');
531
+ expect(result.current.shouldShowCancelledNotice).toBe(true);
532
+ });
533
+ it('should handle multiple cancelled subscriptions with different start dates', () => {
534
+ const sub1 = createSubscription({
535
+ status: 'active',
536
+ cancelledAt: '2024-06-01T00:00:00Z',
537
+ amount: 1000,
538
+ planName: 'Plan 1',
539
+ startDate: '2024-01-01T00:00:00Z',
540
+ });
541
+ const sub2 = createSubscription({
542
+ status: 'active',
543
+ cancelledAt: '2024-06-01T00:00:00Z',
544
+ amount: 2000,
545
+ planName: 'Plan 2',
546
+ startDate: '2024-03-01T00:00:00Z',
547
+ });
548
+ const sub3 = createSubscription({
549
+ status: 'active',
550
+ cancelledAt: '2024-06-01T00:00:00Z',
551
+ amount: 3000,
552
+ planName: 'Plan 3',
553
+ startDate: '2024-02-01T00:00:00Z',
554
+ });
555
+ const subscriptions = [sub1, sub2, sub3];
556
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
557
+ subscriptions,
558
+ loading: false,
559
+ hasPlan: vi.fn(),
560
+ activeSubscription: null,
561
+ hasPaidSubscription: false,
562
+ activePaidSubscription: null,
563
+ refetch: vi.fn(),
564
+ });
565
+ const { result } = renderHook(() => useSubscriptionStatus());
566
+ // Should return the most recent (sub2 - March 1)
567
+ expect(result.current.cancelledSubscription?.planName).toBe('Plan 2');
568
+ expect(result.current.cancelledSubscription?.startDate).toBe('2024-03-01T00:00:00Z');
569
+ });
570
+ it('should memoize functions correctly', () => {
571
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
572
+ subscriptions: [],
573
+ loading: false,
574
+ hasPlan: vi.fn(),
575
+ activeSubscription: null,
576
+ hasPaidSubscription: false,
577
+ activePaidSubscription: null,
578
+ refetch: vi.fn(),
579
+ });
580
+ const { result, rerender } = renderHook(() => useSubscriptionStatus());
581
+ const formatDate1 = result.current.formatDate;
582
+ const getDaysUntilExpiration1 = result.current.getDaysUntilExpiration;
583
+ rerender();
584
+ const formatDate2 = result.current.formatDate;
585
+ const getDaysUntilExpiration2 = result.current.getDaysUntilExpiration;
586
+ // Functions should be stable (same reference) due to useCallback
587
+ expect(formatDate1).toBe(formatDate2);
588
+ expect(getDaysUntilExpiration1).toBe(getDaysUntilExpiration2);
589
+ });
590
+ it('should update cancelledSubscription when subscriptions change', () => {
591
+ const { result, rerender } = renderHook(() => useSubscriptionStatus());
592
+ // Initially no subscriptions
593
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
594
+ subscriptions: [],
595
+ loading: false,
596
+ hasPlan: vi.fn(),
597
+ activeSubscription: null,
598
+ hasPaidSubscription: false,
599
+ activePaidSubscription: null,
600
+ refetch: vi.fn(),
601
+ });
602
+ expect(result.current.cancelledSubscription).toBeNull();
603
+ // Add cancelled subscription
604
+ const cancelledSub = createSubscription({
605
+ status: 'active',
606
+ cancelledAt: '2024-06-01T00:00:00Z',
607
+ amount: 1000,
608
+ });
609
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
610
+ subscriptions: [cancelledSub],
611
+ loading: false,
612
+ hasPlan: vi.fn(),
613
+ activeSubscription: null,
614
+ hasPaidSubscription: false,
615
+ activePaidSubscription: null,
616
+ refetch: vi.fn(),
617
+ });
618
+ rerender();
619
+ expect(result.current.cancelledSubscription).not.toBeNull();
620
+ expect(result.current.shouldShowCancelledNotice).toBe(true);
621
+ });
622
+ it('should handle date formatting with cancelled subscription', () => {
623
+ const cancelledSub = createSubscription({
624
+ status: 'active',
625
+ amount: 1000,
626
+ endDate: '2024-12-31T23:59:59Z',
627
+ cancelledAt: '2024-06-01T00:00:00Z',
628
+ });
629
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
630
+ subscriptions: [cancelledSub],
631
+ loading: false,
632
+ hasPlan: vi.fn(),
633
+ activeSubscription: null,
634
+ hasPaidSubscription: false,
635
+ activePaidSubscription: null,
636
+ refetch: vi.fn(),
637
+ });
638
+ const { result } = renderHook(() => useSubscriptionStatus());
639
+ const formattedEndDate = result.current.formatDate(cancelledSub.endDate);
640
+ const formattedCancelledAt = result.current.formatDate(cancelledSub.cancelledAt);
641
+ const daysLeft = result.current.getDaysUntilExpiration(cancelledSub.endDate);
642
+ expect(formattedEndDate).toBeTruthy();
643
+ expect(formattedCancelledAt).toBeTruthy();
644
+ expect(daysLeft).toBeDefined();
645
+ });
646
+ });
647
+ describe('isPaidSubscription helper (internal)', () => {
648
+ it('should correctly identify paid subscriptions', () => {
649
+ const paidSub = createSubscription({ amount: 1000 });
650
+ const freeSub = createSubscription({ amount: 0 });
651
+ const undefinedSub = createSubscription({ amount: undefined });
652
+ const subscriptions = [paidSub, freeSub, undefinedSub];
653
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
654
+ subscriptions,
655
+ loading: false,
656
+ hasPlan: vi.fn(),
657
+ activeSubscription: null,
658
+ hasPaidSubscription: true,
659
+ activePaidSubscription: paidSub,
660
+ refetch: vi.fn(),
661
+ });
662
+ const { result } = renderHook(() => useSubscriptionStatus());
663
+ // Only paid cancelled subscriptions should be returned
664
+ const cancelledPaid = createSubscription({
665
+ status: 'active',
666
+ cancelledAt: '2024-06-01T00:00:00Z',
667
+ amount: 1000,
668
+ });
669
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
670
+ subscriptions: [cancelledPaid],
671
+ loading: false,
672
+ hasPlan: vi.fn(),
673
+ activeSubscription: null,
674
+ hasPaidSubscription: false,
675
+ activePaidSubscription: null,
676
+ refetch: vi.fn(),
677
+ });
678
+ const { result: result2 } = renderHook(() => useSubscriptionStatus());
679
+ expect(result2.current.cancelledSubscription).not.toBeNull();
680
+ });
681
+ it('should handle zero amount as free', () => {
682
+ const cancelledFree = createSubscription({
683
+ status: 'active',
684
+ cancelledAt: '2024-06-01T00:00:00Z',
685
+ amount: 0,
686
+ });
687
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
688
+ subscriptions: [cancelledFree],
689
+ loading: false,
690
+ hasPlan: vi.fn(),
691
+ activeSubscription: null,
692
+ hasPaidSubscription: false,
693
+ activePaidSubscription: null,
694
+ refetch: vi.fn(),
695
+ });
696
+ const { result } = renderHook(() => useSubscriptionStatus());
697
+ expect(result.current.cancelledSubscription).toBeNull();
698
+ });
699
+ it('should handle undefined amount as free', () => {
700
+ const cancelledUndefined = createSubscription({
701
+ status: 'active',
702
+ cancelledAt: '2024-06-01T00:00:00Z',
703
+ amount: undefined,
704
+ });
705
+ vi.mocked(useSubscriptionModule.useSubscription).mockReturnValue({
706
+ subscriptions: [cancelledUndefined],
707
+ loading: false,
708
+ hasPlan: vi.fn(),
709
+ activeSubscription: null,
710
+ hasPaidSubscription: false,
711
+ activePaidSubscription: null,
712
+ refetch: vi.fn(),
713
+ });
714
+ const { result } = renderHook(() => useSubscriptionStatus());
715
+ expect(result.current.cancelledSubscription).toBeNull();
716
+ });
717
+ });
718
+ });
719
+ //# sourceMappingURL=useSubscriptionStatus.test.js.map