@umituz/react-native-subscription 1.0.7 → 1.1.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 (83) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/lib/application/ports/ISubscriptionRepository.d.ts +25 -0
  4. package/lib/application/ports/ISubscriptionRepository.d.ts.map +1 -0
  5. package/lib/application/ports/ISubscriptionRepository.js +9 -0
  6. package/lib/application/ports/ISubscriptionRepository.js.map +1 -0
  7. package/lib/application/ports/ISubscriptionService.d.ts +28 -0
  8. package/lib/application/ports/ISubscriptionService.d.ts.map +1 -0
  9. package/lib/application/ports/ISubscriptionService.js +6 -0
  10. package/lib/application/ports/ISubscriptionService.js.map +1 -0
  11. package/lib/domain/entities/SubscriptionStatus.d.ts +31 -0
  12. package/lib/domain/entities/SubscriptionStatus.d.ts.map +1 -0
  13. package/lib/domain/entities/SubscriptionStatus.js +39 -0
  14. package/lib/domain/entities/SubscriptionStatus.js.map +1 -0
  15. package/lib/domain/errors/SubscriptionError.d.ts +18 -0
  16. package/lib/domain/errors/SubscriptionError.d.ts.map +1 -0
  17. package/lib/domain/errors/SubscriptionError.js +30 -0
  18. package/lib/domain/errors/SubscriptionError.js.map +1 -0
  19. package/lib/domain/value-objects/SubscriptionConfig.d.ts +15 -0
  20. package/lib/domain/value-objects/SubscriptionConfig.d.ts.map +1 -0
  21. package/lib/domain/value-objects/SubscriptionConfig.js +6 -0
  22. package/lib/domain/value-objects/SubscriptionConfig.js.map +1 -0
  23. package/lib/index.d.ts +33 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +43 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/infrastructure/services/ActivationHandler.d.ts +20 -0
  28. package/lib/infrastructure/services/ActivationHandler.d.ts.map +1 -0
  29. package/lib/infrastructure/services/ActivationHandler.js +71 -0
  30. package/lib/infrastructure/services/ActivationHandler.js.map +1 -0
  31. package/lib/infrastructure/services/SubscriptionService.d.ts +22 -0
  32. package/lib/infrastructure/services/SubscriptionService.d.ts.map +1 -0
  33. package/lib/infrastructure/services/SubscriptionService.js +110 -0
  34. package/lib/infrastructure/services/SubscriptionService.js.map +1 -0
  35. package/lib/presentation/hooks/useSubscription.d.ts +33 -0
  36. package/lib/presentation/hooks/useSubscription.d.ts.map +1 -0
  37. package/lib/presentation/hooks/useSubscription.js +129 -0
  38. package/lib/presentation/hooks/useSubscription.js.map +1 -0
  39. package/lib/utils/dateUtils.d.ts +39 -0
  40. package/lib/utils/dateUtils.d.ts.map +1 -0
  41. package/lib/utils/dateUtils.js +117 -0
  42. package/lib/utils/dateUtils.js.map +1 -0
  43. package/lib/utils/dateValidationUtils.d.ts +20 -0
  44. package/lib/utils/dateValidationUtils.d.ts.map +1 -0
  45. package/lib/utils/dateValidationUtils.js +39 -0
  46. package/lib/utils/dateValidationUtils.js.map +1 -0
  47. package/lib/utils/periodUtils.d.ts +38 -0
  48. package/lib/utils/periodUtils.d.ts.map +1 -0
  49. package/lib/utils/periodUtils.js +70 -0
  50. package/lib/utils/periodUtils.js.map +1 -0
  51. package/lib/utils/planDetectionUtils.d.ts +17 -0
  52. package/lib/utils/planDetectionUtils.d.ts.map +1 -0
  53. package/lib/utils/planDetectionUtils.js +31 -0
  54. package/lib/utils/planDetectionUtils.js.map +1 -0
  55. package/lib/utils/priceUtils.d.ts +23 -0
  56. package/lib/utils/priceUtils.d.ts.map +1 -0
  57. package/lib/utils/priceUtils.js +29 -0
  58. package/lib/utils/priceUtils.js.map +1 -0
  59. package/lib/utils/subscriptionConstants.d.ts +62 -0
  60. package/lib/utils/subscriptionConstants.d.ts.map +1 -0
  61. package/lib/utils/subscriptionConstants.js +61 -0
  62. package/lib/utils/subscriptionConstants.js.map +1 -0
  63. package/package.json +13 -3
  64. package/src/application/ports/ISubscriptionRepository.ts +0 -0
  65. package/src/application/ports/ISubscriptionService.ts +0 -0
  66. package/src/domain/entities/SubscriptionStatus.test.ts +106 -0
  67. package/src/domain/entities/SubscriptionStatus.ts +0 -0
  68. package/src/domain/errors/SubscriptionError.ts +0 -0
  69. package/src/domain/value-objects/SubscriptionConfig.ts +0 -0
  70. package/src/index.ts +9 -2
  71. package/src/infrastructure/services/ActivationHandler.ts +8 -0
  72. package/src/infrastructure/services/SubscriptionService.ts +13 -1
  73. package/src/presentation/hooks/useSubscription.ts +22 -2
  74. package/src/utils/dateUtils.test.ts +116 -0
  75. package/src/utils/dateUtils.ts +12 -76
  76. package/src/utils/dateValidationUtils.test.ts +142 -0
  77. package/src/utils/dateValidationUtils.ts +53 -0
  78. package/src/utils/periodUtils.ts +0 -0
  79. package/src/utils/planDetectionUtils.test.ts +47 -0
  80. package/src/utils/planDetectionUtils.ts +40 -0
  81. package/src/utils/priceUtils.test.ts +35 -0
  82. package/src/utils/priceUtils.ts +0 -0
  83. package/src/utils/subscriptionConstants.ts +0 -0
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Subscription Constants
3
+ * Centralized constants for subscription operations
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only constants, no logic
7
+ * - DRY: All constants in one place
8
+ * - KISS: Simple, clear constant definitions
9
+ */
10
+ /**
11
+ * Subscription plan types
12
+ */
13
+ export declare const SUBSCRIPTION_PLAN_TYPES: {
14
+ readonly WEEKLY: "weekly";
15
+ readonly MONTHLY: "monthly";
16
+ readonly YEARLY: "yearly";
17
+ readonly UNKNOWN: "unknown";
18
+ };
19
+ export type SubscriptionPlanType = (typeof SUBSCRIPTION_PLAN_TYPES)[keyof typeof SUBSCRIPTION_PLAN_TYPES];
20
+ /**
21
+ * Minimum expected subscription durations in days
22
+ * Used to detect sandbox accelerated timers
23
+ * Includes 1 day tolerance for clock skew
24
+ */
25
+ export declare const MIN_SUBSCRIPTION_DURATIONS_DAYS: {
26
+ readonly WEEKLY: 6;
27
+ readonly MONTHLY: 28;
28
+ readonly YEARLY: 360;
29
+ readonly UNKNOWN: 28;
30
+ };
31
+ /**
32
+ * Subscription period multipliers
33
+ * Days to add for each subscription type
34
+ */
35
+ export declare const SUBSCRIPTION_PERIOD_DAYS: {
36
+ readonly WEEKLY: 7;
37
+ };
38
+ /**
39
+ * Date calculation constants
40
+ */
41
+ export declare const DATE_CONSTANTS: {
42
+ readonly MILLISECONDS_PER_DAY: number;
43
+ readonly DEFAULT_LOCALE: "en-US";
44
+ };
45
+ /**
46
+ * Subscription period unit mappings
47
+ * Maps RevenueCat period units to our internal types
48
+ */
49
+ export declare const SUBSCRIPTION_PERIOD_UNITS: {
50
+ readonly WEEK: "WEEK";
51
+ readonly MONTH: "MONTH";
52
+ readonly YEAR: "YEAR";
53
+ };
54
+ /**
55
+ * Product ID keywords for plan detection
56
+ */
57
+ export declare const PRODUCT_ID_KEYWORDS: {
58
+ readonly WEEKLY: readonly ["weekly", "week"];
59
+ readonly MONTHLY: readonly ["monthly", "month"];
60
+ readonly YEARLY: readonly ["yearly", "year", "annual"];
61
+ };
62
+ //# sourceMappingURL=subscriptionConstants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptionConstants.d.ts","sourceRoot":"","sources":["../../src/utils/subscriptionConstants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,eAAO,MAAM,uBAAuB;;;;;CAK1B,CAAC;AAEX,MAAM,MAAM,oBAAoB,GAC9B,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,OAAO,uBAAuB,CAAC,CAAC;AAEzE;;;;GAIG;AACH,eAAO,MAAM,+BAA+B;;;;;CAKlC,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,wBAAwB;;CAE3B,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;CAI5B,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;CAItB,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Subscription Constants
3
+ * Centralized constants for subscription operations
4
+ *
5
+ * Following SOLID, DRY, KISS principles:
6
+ * - Single Responsibility: Only constants, no logic
7
+ * - DRY: All constants in one place
8
+ * - KISS: Simple, clear constant definitions
9
+ */
10
+ /**
11
+ * Subscription plan types
12
+ */
13
+ export const SUBSCRIPTION_PLAN_TYPES = {
14
+ WEEKLY: 'weekly',
15
+ MONTHLY: 'monthly',
16
+ YEARLY: 'yearly',
17
+ UNKNOWN: 'unknown',
18
+ };
19
+ /**
20
+ * Minimum expected subscription durations in days
21
+ * Used to detect sandbox accelerated timers
22
+ * Includes 1 day tolerance for clock skew
23
+ */
24
+ export const MIN_SUBSCRIPTION_DURATIONS_DAYS = {
25
+ WEEKLY: 6,
26
+ MONTHLY: 28,
27
+ YEARLY: 360,
28
+ UNKNOWN: 28,
29
+ };
30
+ /**
31
+ * Subscription period multipliers
32
+ * Days to add for each subscription type
33
+ */
34
+ export const SUBSCRIPTION_PERIOD_DAYS = {
35
+ WEEKLY: 7,
36
+ };
37
+ /**
38
+ * Date calculation constants
39
+ */
40
+ export const DATE_CONSTANTS = {
41
+ MILLISECONDS_PER_DAY: 1000 * 60 * 60 * 24,
42
+ DEFAULT_LOCALE: 'en-US',
43
+ };
44
+ /**
45
+ * Subscription period unit mappings
46
+ * Maps RevenueCat period units to our internal types
47
+ */
48
+ export const SUBSCRIPTION_PERIOD_UNITS = {
49
+ WEEK: 'WEEK',
50
+ MONTH: 'MONTH',
51
+ YEAR: 'YEAR',
52
+ };
53
+ /**
54
+ * Product ID keywords for plan detection
55
+ */
56
+ export const PRODUCT_ID_KEYWORDS = {
57
+ WEEKLY: ['weekly', 'week'],
58
+ MONTHLY: ['monthly', 'month'],
59
+ YEARLY: ['yearly', 'year', 'annual'],
60
+ };
61
+ //# sourceMappingURL=subscriptionConstants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptionConstants.js","sourceRoot":"","sources":["../../src/utils/subscriptionConstants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;CACV,CAAC;AAKX;;;;GAIG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC7C,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,GAAG;IACX,OAAO,EAAE,EAAE;CACH,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,MAAM,EAAE,CAAC;CACD,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,oBAAoB,EAAE,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE;IACzC,cAAc,EAAE,OAAO;CACf,CAAC;AAEX;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,MAAM;CACJ,CAAC;AAEX;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC;IAC7B,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;CAC5B,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "Subscription management system for React Native apps - Database-first approach with secure validation",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
7
  "scripts": {
8
+ "build": "tsc",
8
9
  "typecheck": "tsc --noEmit",
9
10
  "lint": "tsc --noEmit",
11
+ "test": "jest",
12
+ "test:watch": "jest --watch",
13
+ "test:coverage": "jest --coverage",
14
+ "prepublishOnly": "npm run build",
10
15
  "version:patch": "npm version patch -m 'chore: release v%s'",
11
16
  "version:minor": "npm version minor -m 'chore: release v%s'",
12
17
  "version:major": "npm version major -m 'chore: release v%s'"
@@ -37,16 +42,21 @@
37
42
  "react-native": ">=0.74.0"
38
43
  },
39
44
  "devDependencies": {
45
+ "@types/jest": "^29.5.14",
40
46
  "@types/react": "^18.2.45",
41
47
  "@types/react-native": "^0.73.0",
48
+ "find-up": "^8.0.0",
49
+ "jest": "^29.7.0",
42
50
  "react": "^18.2.0",
43
51
  "react-native": "^0.74.0",
52
+ "ts-jest": "^29.4.6",
44
53
  "typescript": "^5.3.3"
45
54
  },
46
55
  "publishConfig": {
47
56
  "access": "public"
48
57
  },
49
58
  "files": [
59
+ "lib",
50
60
  "src",
51
61
  "README.md",
52
62
  "LICENSE"
File without changes
File without changes
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tests for Subscription Status Entity
3
+ */
4
+
5
+ import {
6
+ createDefaultSubscriptionStatus,
7
+ isSubscriptionValid,
8
+ } from './SubscriptionStatus';
9
+
10
+ describe('SubscriptionStatus', () => {
11
+ describe('createDefaultSubscriptionStatus', () => {
12
+ it('should create default subscription status', () => {
13
+ const status = createDefaultSubscriptionStatus();
14
+
15
+ expect(status).toEqual({
16
+ isPremium: false,
17
+ expiresAt: null,
18
+ productId: null,
19
+ purchasedAt: null,
20
+ customerId: null,
21
+ syncedAt: null,
22
+ });
23
+ });
24
+ });
25
+
26
+ describe('isSubscriptionValid', () => {
27
+ it('should return false for null status', () => {
28
+ expect(isSubscriptionValid(null)).toBe(false);
29
+ });
30
+
31
+ it('should return false for non-premium status', () => {
32
+ const status = {
33
+ isPremium: false,
34
+ expiresAt: null,
35
+ productId: null,
36
+ purchasedAt: null,
37
+ customerId: null,
38
+ syncedAt: null,
39
+ };
40
+
41
+ expect(isSubscriptionValid(status)).toBe(false);
42
+ });
43
+
44
+ it('should return true for lifetime subscription', () => {
45
+ const status = {
46
+ isPremium: true,
47
+ expiresAt: null,
48
+ productId: 'lifetime',
49
+ purchasedAt: '2024-01-01T00:00:00.000Z',
50
+ customerId: 'customer123',
51
+ syncedAt: '2024-01-01T00:00:00.000Z',
52
+ };
53
+
54
+ expect(isSubscriptionValid(status)).toBe(true);
55
+ });
56
+
57
+ it('should return true for active subscription', () => {
58
+ const futureDate = new Date();
59
+ futureDate.setDate(futureDate.getDate() + 30);
60
+
61
+ const status = {
62
+ isPremium: true,
63
+ expiresAt: futureDate.toISOString(),
64
+ productId: 'monthly',
65
+ purchasedAt: '2024-01-01T00:00:00.000Z',
66
+ customerId: 'customer123',
67
+ syncedAt: '2024-01-01T00:00:00.000Z',
68
+ };
69
+
70
+ expect(isSubscriptionValid(status)).toBe(true);
71
+ });
72
+
73
+ it('should return true for subscription expired within 24 hour buffer', () => {
74
+ const pastDate = new Date();
75
+ pastDate.setDate(pastDate.getDate() - 1);
76
+ pastDate.setHours(pastDate.getHours() + 1); // 23 hours ago
77
+
78
+ const status = {
79
+ isPremium: true,
80
+ expiresAt: pastDate.toISOString(),
81
+ productId: 'monthly',
82
+ purchasedAt: '2024-01-01T00:00:00.000Z',
83
+ customerId: 'customer123',
84
+ syncedAt: '2024-01-01T00:00:00.000Z',
85
+ };
86
+
87
+ expect(isSubscriptionValid(status)).toBe(true);
88
+ });
89
+
90
+ it('should return false for expired subscription beyond buffer', () => {
91
+ const pastDate = new Date();
92
+ pastDate.setDate(pastDate.getDate() - 2);
93
+
94
+ const status = {
95
+ isPremium: true,
96
+ expiresAt: pastDate.toISOString(),
97
+ productId: 'monthly',
98
+ purchasedAt: '2024-01-01T00:00:00.000Z',
99
+ customerId: 'customer123',
100
+ syncedAt: '2024-01-01T00:00:00.000Z',
101
+ };
102
+
103
+ expect(isSubscriptionValid(status)).toBe(false);
104
+ });
105
+ });
106
+ });
File without changes
File without changes
File without changes
package/src/index.ts CHANGED
@@ -66,12 +66,19 @@ export type { UseSubscriptionResult } from './presentation/hooks/useSubscription
66
66
 
67
67
  // Date utilities
68
68
  export {
69
- isSubscriptionExpired,
70
- getDaysUntilExpiration,
71
69
  formatExpirationDate,
72
70
  calculateExpirationDate,
73
71
  } from './utils/dateUtils';
74
72
 
73
+ export {
74
+ isSubscriptionExpired,
75
+ getDaysUntilExpiration,
76
+ } from './utils/dateValidationUtils';
77
+
78
+ export {
79
+ extractPlanFromProductId,
80
+ } from './utils/planDetectionUtils';
81
+
75
82
  // Price utilities
76
83
  export { formatPrice } from './utils/priceUtils';
77
84
 
@@ -26,6 +26,10 @@ export async function activateSubscription(
26
26
  expiresAt: string | null
27
27
  ): Promise<SubscriptionStatus> {
28
28
  try {
29
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
30
+ console.log("[Subscription] Activating subscription in handler", { userId, productId, expiresAt });
31
+ }
32
+
29
33
  const updatedStatus = await config.repository.updateSubscriptionStatus(
30
34
  userId,
31
35
  {
@@ -53,6 +57,10 @@ export async function deactivateSubscription(
53
57
  userId: string
54
58
  ): Promise<SubscriptionStatus> {
55
59
  try {
60
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
61
+ console.log("[Subscription] Deactivating subscription in handler", { userId });
62
+ }
63
+
56
64
  const updatedStatus = await config.repository.updateSubscriptionStatus(
57
65
  userId,
58
66
  {
@@ -39,11 +39,17 @@ export class SubscriptionService implements ISubscriptionService {
39
39
  try {
40
40
  const status = await this.repository.getSubscriptionStatus(userId);
41
41
  if (!status) {
42
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
43
+ console.log("[Subscription] No status found for user, returning default");
44
+ }
42
45
  return createDefaultSubscriptionStatus();
43
46
  }
44
47
 
45
48
  const isValid = this.repository.isSubscriptionValid(status);
46
49
  if (!isValid && status.isPremium) {
50
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
51
+ console.log("[Subscription] Expired subscription found, deactivating");
52
+ }
47
53
  return await this.deactivateSubscription(userId);
48
54
  }
49
55
 
@@ -64,6 +70,9 @@ export class SubscriptionService implements ISubscriptionService {
64
70
  productId: string,
65
71
  expiresAt: string | null
66
72
  ): Promise<SubscriptionStatus> {
73
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
74
+ console.log("[Subscription] Activating subscription", { userId, productId, expiresAt });
75
+ }
67
76
  return activateSubscription(
68
77
  this.handlerConfig,
69
78
  userId,
@@ -73,6 +82,9 @@ export class SubscriptionService implements ISubscriptionService {
73
82
  }
74
83
 
75
84
  async deactivateSubscription(userId: string): Promise<SubscriptionStatus> {
85
+ if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
86
+ console.log("[Subscription] Deactivating subscription", { userId });
87
+ }
76
88
  return deactivateSubscription(this.handlerConfig, userId);
77
89
  }
78
90
 
@@ -130,7 +142,7 @@ export function initializeSubscriptionService(
130
142
  }
131
143
 
132
144
  export function getSubscriptionService(): SubscriptionService | null {
133
- if (!subscriptionServiceInstance && typeof __DEV__ !== "undefined" && __DEV__) {
145
+ if (!subscriptionServiceInstance && typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
134
146
  // eslint-disable-next-line no-console
135
147
  console.warn("[Subscription] Service not initialized");
136
148
  }
@@ -3,7 +3,7 @@
3
3
  * React hook for subscription management
4
4
  */
5
5
 
6
- import { useState, useCallback, useEffect } from 'react';
6
+ import { useState, useCallback } from 'react';
7
7
  import { getSubscriptionService } from '../../infrastructure/services/SubscriptionService';
8
8
  import type { SubscriptionStatus } from '../../domain/entities/SubscriptionStatus';
9
9
 
@@ -44,6 +44,11 @@ export function useSubscription(): UseSubscriptionResult {
44
44
  const [error, setError] = useState<string | null>(null);
45
45
 
46
46
  const loadStatus = useCallback(async (userId: string) => {
47
+ if (!userId) {
48
+ setError('User ID is required');
49
+ return;
50
+ }
51
+
47
52
  const service = getSubscriptionService();
48
53
  if (!service) {
49
54
  setError('Subscription service is not initialized');
@@ -66,6 +71,11 @@ export function useSubscription(): UseSubscriptionResult {
66
71
  }, []);
67
72
 
68
73
  const refreshStatus = useCallback(async (userId: string) => {
74
+ if (!userId) {
75
+ setError('User ID is required');
76
+ return;
77
+ }
78
+
69
79
  const service = getSubscriptionService();
70
80
  if (!service) {
71
81
  setError('Subscription service is not initialized');
@@ -89,6 +99,11 @@ export function useSubscription(): UseSubscriptionResult {
89
99
 
90
100
  const activateSubscription = useCallback(
91
101
  async (userId: string, productId: string, expiresAt: string | null) => {
102
+ if (!userId || !productId) {
103
+ setError('User ID and Product ID are required');
104
+ return;
105
+ }
106
+
92
107
  const service = getSubscriptionService();
93
108
  if (!service) {
94
109
  setError('Subscription service is not initialized');
@@ -118,6 +133,11 @@ export function useSubscription(): UseSubscriptionResult {
118
133
  );
119
134
 
120
135
  const deactivateSubscription = useCallback(async (userId: string) => {
136
+ if (!userId) {
137
+ setError('User ID is required');
138
+ return;
139
+ }
140
+
121
141
  const service = getSubscriptionService();
122
142
  if (!service) {
123
143
  setError('Subscription service is not initialized');
@@ -140,7 +160,7 @@ export function useSubscription(): UseSubscriptionResult {
140
160
  }
141
161
  }, []);
142
162
 
143
- const isPremium = status ? status.isPremium && (status.expiresAt === null || new Date(status.expiresAt) > new Date()) : false;
163
+ const isPremium = status?.isPremium && (status.expiresAt === null || new Date(status.expiresAt).getTime() > Date.now()) || false;
144
164
 
145
165
  return {
146
166
  status,
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tests for Date Utilities
3
+ */
4
+
5
+ import {
6
+ formatExpirationDate,
7
+ calculateExpirationDate,
8
+ } from '../utils/dateUtils';
9
+ import { SUBSCRIPTION_PLAN_TYPES } from '../utils/subscriptionConstants';
10
+
11
+ describe('Date Utils', () => {
12
+ describe('formatExpirationDate', () => {
13
+ it('should return null for null expiration', () => {
14
+ expect(formatExpirationDate(null)).toBeNull();
15
+ });
16
+
17
+ it('should format date correctly', () => {
18
+ const date = '2024-12-25T00:00:00.000Z';
19
+ const formatted = formatExpirationDate(date);
20
+
21
+ expect(formatted).toMatch(/December 25, 2024/);
22
+ });
23
+
24
+ it('should use custom locale', () => {
25
+ const date = '2024-12-25T00:00:00.000Z';
26
+ const formatted = formatExpirationDate(date, 'tr-TR');
27
+
28
+ expect(formatted).toBeTruthy();
29
+ expect(typeof formatted).toBe('string');
30
+ });
31
+
32
+ it('should return null for invalid date', () => {
33
+ const formatted = formatExpirationDate('invalid-date');
34
+ expect(formatted).toBeNull();
35
+ });
36
+ });
37
+
38
+ describe('calculateExpirationDate', () => {
39
+ beforeEach(() => {
40
+ // Mock current date for consistent testing
41
+ jest.useFakeTimers();
42
+ jest.setSystemTime(new Date('2024-01-15T00:00:00.000Z'));
43
+ });
44
+
45
+ afterEach(() => {
46
+ jest.useRealTimers();
47
+ });
48
+
49
+ it('should return null for null productId', () => {
50
+ expect(calculateExpirationDate(null)).toBeNull();
51
+ expect(calculateExpirationDate('')).toBeNull();
52
+ expect(calculateExpirationDate(undefined)).toBeNull();
53
+ });
54
+
55
+ it('should calculate weekly expiration', () => {
56
+ const result = calculateExpirationDate('com.app.weekly');
57
+ const expectedDate = new Date('2024-01-22T00:00:00.000Z');
58
+
59
+ expect(result).toBe(expectedDate.toISOString());
60
+ });
61
+
62
+ it('should calculate monthly expiration', () => {
63
+ const result = calculateExpirationDate('com.app.monthly');
64
+ const expectedDate = new Date('2024-02-15T00:00:00.000Z');
65
+
66
+ expect(result).toBe(expectedDate.toISOString());
67
+ });
68
+
69
+ it('should calculate yearly expiration', () => {
70
+ const result = calculateExpirationDate('com.app.yearly');
71
+ const expectedDate = new Date('2025-01-15T00:00:00.000Z');
72
+
73
+ expect(result).toBe(expectedDate.toISOString());
74
+ });
75
+
76
+ it('should default to monthly for unknown plan', () => {
77
+ const result = calculateExpirationDate('com.app.unknown');
78
+ const expectedDate = new Date('2024-02-15T00:00:00.000Z');
79
+
80
+ expect(result).toBe(expectedDate.toISOString());
81
+ });
82
+
83
+ it('should trust valid RevenueCat date', () => {
84
+ const futureDate = new Date('2024-02-20T00:00:00.000Z');
85
+ const result = calculateExpirationDate('com.app.monthly', futureDate.toISOString());
86
+
87
+ expect(result).toBe(futureDate.toISOString());
88
+ });
89
+
90
+ it('should ignore RevenueCat date if too short (sandbox)', () => {
91
+ const nearFutureDate = new Date('2024-01-16T00:00:00.000Z'); // Only 1 day
92
+ const result = calculateExpirationDate('com.app.monthly', nearFutureDate.toISOString());
93
+
94
+ // Should calculate manually instead of using RevenueCat date
95
+ const expectedDate = new Date('2024-02-15T00:00:00.000Z');
96
+ expect(result).toBe(expectedDate.toISOString());
97
+ });
98
+
99
+ it('should ignore past RevenueCat date', () => {
100
+ const pastDate = new Date('2024-01-10T00:00:00.000Z');
101
+ const result = calculateExpirationDate('com.app.monthly', pastDate.toISOString());
102
+
103
+ // Should calculate manually instead of using RevenueCat date
104
+ const expectedDate = new Date('2024-02-15T00:00:00.000Z');
105
+ expect(result).toBe(expectedDate.toISOString());
106
+ });
107
+
108
+ it('should handle invalid RevenueCat date', () => {
109
+ const result = calculateExpirationDate('com.app.monthly', 'invalid-date');
110
+
111
+ // Should calculate manually instead of using RevenueCat date
112
+ const expectedDate = new Date('2024-02-15T00:00:00.000Z');
113
+ expect(result).toBe(expectedDate.toISOString());
114
+ });
115
+ });
116
+ });