@umituz/react-native-subscription 2.14.90 → 2.14.91

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.14.90",
3
+ "version": "2.14.91",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,9 +1,7 @@
1
- /**
2
- * Tests for Subscription Status Entity
3
- */
4
-
1
+ import {
5
2
  createDefaultSubscriptionStatus,
6
3
  isSubscriptionValid,
4
+ calculateDaysRemaining,
7
5
  } from './SubscriptionStatus';
8
6
 
9
7
  describe('SubscriptionStatus', () => {
@@ -18,6 +16,7 @@ describe('SubscriptionStatus', () => {
18
16
  purchasedAt: null,
19
17
  customerId: null,
20
18
  syncedAt: null,
19
+ status: 'none',
21
20
  });
22
21
  });
23
22
  });
@@ -69,10 +68,9 @@ describe('SubscriptionStatus', () => {
69
68
  expect(isSubscriptionValid(status)).toBe(true);
70
69
  });
71
70
 
72
- it('should return true for subscription expired within 24 hour buffer', () => {
71
+ it('should return false for expired subscription', () => {
73
72
  const pastDate = new Date();
74
73
  pastDate.setDate(pastDate.getDate() - 1);
75
- pastDate.setHours(pastDate.getHours() + 1); // 23 hours ago
76
74
 
77
75
  const status = {
78
76
  isPremium: true,
@@ -83,23 +81,25 @@ describe('SubscriptionStatus', () => {
83
81
  syncedAt: '2024-01-01T00:00:00.000Z',
84
82
  };
85
83
 
86
- expect(isSubscriptionValid(status)).toBe(true);
84
+ expect(isSubscriptionValid(status)).toBe(false);
85
+ });
86
+ });
87
+
88
+ describe('calculateDaysRemaining', () => {
89
+ it('should return null for null input', () => {
90
+ expect(calculateDaysRemaining(null)).toBeNull();
87
91
  });
88
92
 
89
- it('should return false for expired subscription beyond buffer', () => {
93
+ it('should return positive days for future expiration', () => {
94
+ const futureDate = new Date();
95
+ futureDate.setDate(futureDate.getDate() + 5);
96
+ expect(calculateDaysRemaining(futureDate.toISOString())).toBe(5);
97
+ });
98
+
99
+ it('should return 0 for past expiration', () => {
90
100
  const pastDate = new Date();
91
- pastDate.setDate(pastDate.getDate() - 2);
92
-
93
- const status = {
94
- isPremium: true,
95
- expiresAt: pastDate.toISOString(),
96
- productId: 'monthly',
97
- purchasedAt: '2024-01-01T00:00:00.000Z',
98
- customerId: 'customer123',
99
- syncedAt: '2024-01-01T00:00:00.000Z',
100
- };
101
-
102
- expect(isSubscriptionValid(status)).toBe(false);
101
+ pastDate.setDate(pastDate.getDate() - 5);
102
+ expect(calculateDaysRemaining(pastDate.toISOString())).toBe(0);
103
103
  });
104
104
  });
105
105
  });
@@ -33,13 +33,13 @@ export const createDefaultSubscriptionStatus = (): SubscriptionStatus => ({
33
33
 
34
34
  export const isSubscriptionValid = (status: SubscriptionStatus | null): boolean => {
35
35
  if (!status || !status.isPremium) return false;
36
-
37
36
  if (!status.expiresAt) return true; // Lifetime
37
+ return new Date(status.expiresAt).getTime() > Date.now();
38
+ };
38
39
 
39
- const expirationDate = new Date(status.expiresAt);
40
- const now = new Date();
41
-
42
- // Add 24-hour grace period buffer
43
- const buffer = 24 * 60 * 60 * 1000;
44
- return expirationDate.getTime() + buffer > now.getTime();
40
+ export const calculateDaysRemaining = (expiresAt: string | null): number | null => {
41
+ if (!expiresAt) return null;
42
+ const diff = new Date(expiresAt).getTime() - Date.now();
43
+ return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
45
44
  };
45
+
@@ -2,10 +2,11 @@ import { useMemo } from "react";
2
2
  import {
3
3
  type SubscriptionStatus,
4
4
  SUBSCRIPTION_STATUS,
5
- type SubscriptionStatusType
5
+ type SubscriptionStatusType,
6
+ isSubscriptionValid,
7
+ calculateDaysRemaining
6
8
  } from "../../domain/entities/SubscriptionStatus";
7
- import { isSubscriptionExpired } from "../../utils/dateValidationUtils";
8
- import { formatDateForLocale, calculateDaysRemaining } from "../utils/subscriptionDateUtils";
9
+ import { formatDateForLocale } from "../utils/subscriptionDateUtils";
9
10
 
10
11
  export interface SubscriptionDetails {
11
12
  /** Raw subscription status */
@@ -53,10 +54,11 @@ export function useSubscriptionDetails(
53
54
  };
54
55
  }
55
56
 
56
- const isExpired = isSubscriptionExpired(status);
57
+ const isValid = isSubscriptionValid(status);
58
+ const isExpired = status.isPremium && !isValid;
57
59
  const isLifetime = status.isPremium && !status.expiresAt;
58
60
  const daysRemainingValue = calculateDaysRemaining(status.expiresAt ?? null);
59
- const isPremium = status.isPremium && !isExpired;
61
+ const isPremium = status.isPremium && isValid;
60
62
 
61
63
  let statusKey: SubscriptionStatusType = status.status || SUBSCRIPTION_STATUS.NONE;
62
64
 
@@ -9,12 +9,9 @@ import { useCredits } from "./useCredits";
9
9
  import { useSubscriptionStatus } from "./useSubscriptionStatus";
10
10
  import { useCustomerInfo } from "../../revenuecat/presentation/hooks/useCustomerInfo";
11
11
  import { usePaywallVisibility } from "./usePaywallVisibility";
12
+ import { calculateDaysRemaining } from "../../domain/entities/SubscriptionStatus";
12
13
  import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
13
- import {
14
- convertPurchasedAt,
15
- formatDateForLocale,
16
- calculateDaysRemaining,
17
- } from "../utils/subscriptionDateUtils";
14
+ import { formatDateForLocale, convertPurchasedAt } from "../utils/subscriptionDateUtils";
18
15
  import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
19
16
  import type {
20
17
  SubscriptionSettingsConfig,
@@ -68,7 +65,7 @@ export const useSubscriptionSettingsConfig = (
68
65
  // Get expiration date from RevenueCat entitlement (source of truth)
69
66
  // premiumEntitlement.expirationDate is an ISO string from RevenueCat
70
67
  const entitlementExpirationDate = premiumEntitlement?.expirationDate || null;
71
-
68
+
72
69
  // Prefer CustomerInfo expiration (real-time) over cached status
73
70
  const expiresAtIso = entitlementExpirationDate || (statusExpirationDate
74
71
  ? statusExpirationDate.toISOString()
@@ -43,19 +43,4 @@ export const formatDateForLocale = (
43
43
  }
44
44
  };
45
45
 
46
- /**
47
- * Calculates days remaining until expiration
48
- */
49
- export const calculateDaysRemaining = (
50
- expiresAtIso: string | null
51
- ): number | null => {
52
- if (!expiresAtIso) return null;
53
-
54
- const expiresDate = new Date(expiresAtIso);
55
- const now = new Date();
56
-
57
- // Use timezoneService's mathematical approach if possible, or keep existing log
58
- const diff = expiresDate.getTime() - now.getTime();
59
- return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
60
- };
61
46
 
@@ -4,62 +4,16 @@
4
4
  * Centralized logic for authentication checks
5
5
  */
6
6
 
7
- import { validateIsGuest, validateUserId } from './validation';
8
-
9
- /**
10
- * Check if user is authenticated
11
- *
12
- * This is the SINGLE SOURCE OF TRUTH for authentication check.
13
- * All apps should use this function for consistent authentication logic.
14
- *
15
- * @param isGuest - Whether user is a guest
16
- * @param userId - User ID (null for guests)
17
- * @returns Whether user is authenticated
18
- *
19
- * @example
20
- * ```typescript
21
- * // Guest user
22
- * isAuthenticated(true, null); // false
23
- *
24
- * // Authenticated user
25
- * isAuthenticated(false, 'user123'); // true
26
- * ```
27
- */
28
7
  export function isAuthenticated(
29
8
  isGuest: boolean,
30
9
  userId: string | null,
31
10
  ): boolean {
32
- validateIsGuest(isGuest);
33
- validateUserId(userId);
34
-
35
11
  return !isGuest && userId !== null;
36
12
  }
37
13
 
38
- /**
39
- * Check if user is guest
40
- *
41
- * This is the SINGLE SOURCE OF TRUTH for guest check.
42
- * All apps should use this function for consistent guest logic.
43
- *
44
- * @param isGuest - Whether user is a guest
45
- * @param userId - User ID (null for guests)
46
- * @returns Whether user is a guest
47
- *
48
- * @example
49
- * ```typescript
50
- * // Guest user
51
- * isGuest(true, null); // true
52
- *
53
- * // Authenticated user
54
- * isGuest(false, 'user123'); // false
55
- * ```
56
- */
57
14
  export function isGuest(
58
15
  isGuestFlag: boolean,
59
16
  userId: string | null,
60
17
  ): boolean {
61
- validateIsGuest(isGuestFlag);
62
- validateUserId(userId);
63
-
64
18
  return isGuestFlag || userId === null;
65
19
  }
@@ -2,7 +2,6 @@ export * from "./aiCreditHelpers";
2
2
  export * from "./authUtils";
3
3
  export * from "./creditChecker";
4
4
  export * from "./creditMapper";
5
- export * from "./dateValidationUtils";
6
5
  export * from "./packageFilter";
7
6
  export * from "./packagePeriodUtils";
8
7
  export * from "./packageTypeDetector";
@@ -4,58 +4,27 @@
4
4
  * Core premium status determination logic
5
5
  */
6
6
 
7
- import { validateIsGuest, validateUserId, validateFetcher } from './validation';
8
7
  import type { PremiumStatusFetcher } from './types';
9
8
 
10
9
  /**
11
10
  * Get isPremium value with centralized logic
12
- *
13
- * This function handles the complete logic for determining premium status:
14
- * - Guest users NEVER have premium (returns false immediately)
15
- * - Authenticated users: uses provided isPremium value OR fetches using fetcher
16
- *
17
- * This is the SINGLE SOURCE OF TRUTH for isPremium determination.
18
- * All apps should use this function instead of directly calling their premium service.
19
- *
20
- * Two usage modes:
21
- * 1. Sync mode: If you already have isPremium value, pass it directly
22
- * 2. Async mode: If you need to fetch from database, pass a fetcher function
23
- *
24
- * @param isGuest - Whether user is a guest
25
- * @param userId - User ID (null for guests)
26
- * @param isPremiumOrFetcher - Either boolean (sync) or PremiumStatusFetcher (async)
27
- * @returns boolean (sync) or Promise<boolean> (async) - Whether user has premium subscription
28
11
  */
29
12
  export function getIsPremium(
30
13
  isGuestFlag: boolean,
31
14
  userId: string | null,
32
15
  isPremiumOrFetcher: boolean | PremiumStatusFetcher,
33
16
  ): boolean | Promise<boolean> {
34
- validateIsGuest(isGuestFlag);
35
- validateUserId(userId);
36
-
37
- // Guest users NEVER have premium - this is centralized logic
38
- if (isGuestFlag || userId === null) {
39
- return false;
40
- }
41
-
42
- // Check if it's a boolean (sync mode) or fetcher (async mode)
43
- if (typeof isPremiumOrFetcher === 'boolean') {
44
- // Sync mode: return the provided isPremium value
45
- return isPremiumOrFetcher;
46
- }
17
+ // Guest users NEVER have premium
18
+ if (isGuestFlag || userId === null) return false;
47
19
 
48
- // Async mode: validate fetcher and fetch premium status
49
- validateFetcher(isPremiumOrFetcher);
20
+ // Sync mode: return the provided isPremium value
21
+ if (typeof isPremiumOrFetcher === 'boolean') return isPremiumOrFetcher;
50
22
 
51
- // Authenticated users: fetch premium status using app's fetcher
52
- // Package handles the logic, app handles the database operation
23
+ // Async mode: fetch premium status
53
24
  return (async () => {
54
25
  try {
55
26
  return await isPremiumOrFetcher.isPremium(userId);
56
27
  } catch (error) {
57
- // If fetcher throws, assume not premium (fail-safe)
58
- // Apps should handle errors in their fetcher implementation
59
28
  throw new Error(
60
29
  `Failed to fetch premium status: ${error instanceof Error ? error.message : String(error)}`
61
30
  );
@@ -4,39 +4,13 @@
4
4
  * Core logic for determining user tier and premium status
5
5
  */
6
6
 
7
- import { validateIsGuest, validateUserId, validateIsPremium } from './validation';
8
7
  import type { UserTierInfo } from './types';
9
8
 
10
- /**
11
- * Determine user tier from auth state and premium status
12
- *
13
- * This is the SINGLE SOURCE OF TRUTH for tier determination.
14
- * All apps should use this function for consistent tier logic.
15
- *
16
- * @param isGuest - Whether user is a guest
17
- * @param userId - User ID (null for guests)
18
- * @param isPremium - Whether user has active premium subscription
19
- * @returns User tier information
20
- *
21
- * @example
22
- * ```typescript
23
- * const tierInfo = getUserTierInfo(false, 'user123', true);
24
- * // Returns: { tier: 'premium', isPremium: true, isGuest: false, isAuthenticated: true, userId: 'user123' }
25
- *
26
- * const guestInfo = getUserTierInfo(true, null, false);
27
- * // Returns: { tier: 'guest', isPremium: false, isGuest: true, isAuthenticated: false, userId: null }
28
- * ```
29
- */
30
9
  export function getUserTierInfo(
31
10
  isGuestFlag: boolean,
32
11
  userId: string | null,
33
12
  isPremium: boolean,
34
13
  ): UserTierInfo {
35
- validateIsGuest(isGuestFlag);
36
- validateUserId(userId);
37
- validateIsPremium(isPremium);
38
-
39
- // Guest users are always freemium, never premium
40
14
  if (isGuestFlag || userId === null) {
41
15
  return {
42
16
  tier: 'guest',
@@ -47,7 +21,6 @@ export function getUserTierInfo(
47
21
  };
48
22
  }
49
23
 
50
- // Authenticated users: premium or freemium
51
24
  return {
52
25
  tier: isPremium ? 'premium' : 'freemium',
53
26
  isPremium,
@@ -57,41 +30,11 @@ export function getUserTierInfo(
57
30
  };
58
31
  }
59
32
 
60
- /**
61
- * Check if user has premium access (synchronous version)
62
- *
63
- * Guest users NEVER have premium access, regardless of isPremium value.
64
- *
65
- * @param isGuest - Whether user is a guest
66
- * @param userId - User ID (null for guests)
67
- * @param isPremium - Whether user has active premium subscription
68
- * @returns Whether user has premium access
69
- *
70
- * @example
71
- * ```typescript
72
- * // Guest user - always false
73
- * checkPremiumAccess(true, null, true); // false
74
- *
75
- * // Authenticated premium user
76
- * checkPremiumAccess(false, 'user123', true); // true
77
- *
78
- * // Authenticated freemium user
79
- * checkPremiumAccess(false, 'user123', false); // false
80
- * ```
81
- */
82
33
  export function checkPremiumAccess(
83
34
  isGuestFlag: boolean,
84
35
  userId: string | null,
85
36
  isPremium: boolean,
86
37
  ): boolean {
87
- validateIsGuest(isGuestFlag);
88
- validateUserId(userId);
89
- validateIsPremium(isPremium);
90
-
91
- // Guest users never have premium access
92
- if (isGuestFlag || userId === null) {
93
- return false;
94
- }
95
-
38
+ if (isGuestFlag || userId === null) return false;
96
39
  return isPremium;
97
40
  }
@@ -6,43 +6,13 @@
6
6
 
7
7
  import type { UserTier, UserTierInfo } from './types';
8
8
 
9
- /**
10
- * Type guard to check if a value is a valid UserTier
11
- *
12
- * @param value - Value to check
13
- * @returns Whether value is a valid UserTier
14
- *
15
- * @example
16
- * ```typescript
17
- * if (isValidUserTier(someValue)) {
18
- * // TypeScript knows someValue is UserTier
19
- * }
20
- * ```
21
- */
22
9
  export function isValidUserTier(value: unknown): value is UserTier {
23
10
  return value === 'guest' || value === 'freemium' || value === 'premium';
24
11
  }
25
12
 
26
- /**
27
- * Type guard to check if an object is a valid UserTierInfo
28
- *
29
- * @param value - Value to check
30
- * @returns Whether value is a valid UserTierInfo
31
- *
32
- * @example
33
- * ```typescript
34
- * if (isUserTierInfo(someValue)) {
35
- * // TypeScript knows someValue is UserTierInfo
36
- * }
37
- * ```
38
- */
39
13
  export function isUserTierInfo(value: unknown): value is UserTierInfo {
40
- if (typeof value !== 'object' || value === null) {
41
- return false;
42
- }
43
-
14
+ if (typeof value !== 'object' || value === null) return false;
44
15
  const obj = value as Record<string, unknown>;
45
-
46
16
  return (
47
17
  isValidUserTier(obj.tier) &&
48
18
  typeof obj.isPremium === 'boolean' &&
@@ -50,70 +20,4 @@ export function isUserTierInfo(value: unknown): value is UserTierInfo {
50
20
  typeof obj.isAuthenticated === 'boolean' &&
51
21
  (obj.userId === null || typeof obj.userId === 'string')
52
22
  );
53
- }
54
-
55
- /**
56
- * Validate userId parameter
57
- *
58
- * @param userId - User ID to validate
59
- * @throws {TypeError} If userId is invalid
60
- */
61
- export function validateUserId(userId: string | null): void {
62
- if (userId !== null && typeof userId !== 'string') {
63
- throw new TypeError(
64
- `Invalid userId: expected string or null, got ${typeof userId}`
65
- );
66
- }
67
-
68
- if (userId !== null && userId.trim() === '') {
69
- throw new TypeError('Invalid userId: cannot be empty string');
70
- }
71
- }
72
-
73
- /**
74
- * Validate isGuest parameter
75
- *
76
- * @param isGuest - isGuest flag to validate
77
- * @throws {TypeError} If isGuest is invalid
78
- */
79
- export function validateIsGuest(isGuest: boolean): void {
80
- if (typeof isGuest !== 'boolean') {
81
- throw new TypeError(
82
- `Invalid isGuest: expected boolean, got ${typeof isGuest}`
83
- );
84
- }
85
- }
86
-
87
- /**
88
- * Validate isPremium parameter
89
- *
90
- * @param isPremium - isPremium flag to validate
91
- * @throws {TypeError} If isPremium is invalid
92
- */
93
- export function validateIsPremium(isPremium: boolean): void {
94
- if (typeof isPremium !== 'boolean') {
95
- throw new TypeError(
96
- `Invalid isPremium: expected boolean, got ${typeof isPremium}`
97
- );
98
- }
99
- }
100
-
101
- /**
102
- * Validate PremiumStatusFetcher
103
- *
104
- * @param fetcher - Fetcher to validate
105
- * @throws {TypeError} If fetcher is invalid
106
- */
107
- export function validateFetcher(fetcher: import('./types').PremiumStatusFetcher): void {
108
- if (typeof fetcher !== 'object' || fetcher === null) {
109
- throw new TypeError(
110
- `Invalid fetcher: expected object, got ${typeof fetcher}`
111
- );
112
- }
113
-
114
- if (typeof fetcher.isPremium !== 'function') {
115
- throw new TypeError(
116
- 'Invalid fetcher: isPremium must be a function'
117
- );
118
- }
119
23
  }
@@ -1,140 +0,0 @@
1
- /**
2
- * Tests for Date Validation Utilities
3
- */
4
-
5
- isSubscriptionExpired,
6
- getDaysUntilExpiration,
7
- } from '../dateValidationUtils';
8
-
9
- describe('Date Validation Utils', () => {
10
- describe('isSubscriptionExpired', () => {
11
- it('should return true for null status', () => {
12
- expect(isSubscriptionExpired(null)).toBe(true);
13
- });
14
-
15
- it('should return true for non-premium status', () => {
16
- const status = {
17
- isPremium: false,
18
- expiresAt: null,
19
- productId: null,
20
- purchasedAt: null,
21
- customerId: null,
22
- syncedAt: null,
23
- };
24
-
25
- expect(isSubscriptionExpired(status)).toBe(true);
26
- });
27
-
28
- it('should return false for lifetime subscription', () => {
29
- const status = {
30
- isPremium: true,
31
- expiresAt: null,
32
- productId: 'lifetime',
33
- purchasedAt: '2024-01-01T00:00:00.000Z',
34
- customerId: 'customer123',
35
- syncedAt: '2024-01-01T00:00:00.000Z',
36
- };
37
-
38
- expect(isSubscriptionExpired(status)).toBe(false);
39
- });
40
-
41
- it('should return false for future expiration', () => {
42
- const futureDate = new Date();
43
- futureDate.setDate(futureDate.getDate() + 30);
44
-
45
- const status = {
46
- isPremium: true,
47
- expiresAt: futureDate.toISOString(),
48
- productId: 'monthly',
49
- purchasedAt: '2024-01-01T00:00:00.000Z',
50
- customerId: 'customer123',
51
- syncedAt: '2024-01-01T00:00:00.000Z',
52
- };
53
-
54
- expect(isSubscriptionExpired(status)).toBe(false);
55
- });
56
-
57
- it('should return true for past expiration', () => {
58
- const pastDate = new Date();
59
- pastDate.setDate(pastDate.getDate() - 1);
60
-
61
- const status = {
62
- isPremium: true,
63
- expiresAt: pastDate.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(getDaysUntilExpiration(status)).toBe(0);
71
- });
72
- });
73
-
74
- describe('getDaysUntilExpiration', () => {
75
- it('should return null for null status', () => {
76
- expect(getDaysUntilExpiration(null)).toBeNull();
77
- });
78
-
79
- it('should return null for status without expiration', () => {
80
- const status = {
81
- isPremium: true,
82
- expiresAt: null,
83
- productId: 'lifetime',
84
- purchasedAt: '2024-01-01T00:00:00.000Z',
85
- customerId: 'customer123',
86
- syncedAt: '2024-01-01T00:00:00.000Z',
87
- };
88
-
89
- expect(getDaysUntilExpiration(status)).toBeNull();
90
- });
91
-
92
- it('should return positive days for future expiration', () => {
93
- const futureDate = new Date();
94
- futureDate.setDate(futureDate.getDate() + 5);
95
-
96
- const status = {
97
- isPremium: true,
98
- expiresAt: futureDate.toISOString(),
99
- productId: 'monthly',
100
- purchasedAt: '2024-01-01T00:00:00.000Z',
101
- customerId: 'customer123',
102
- syncedAt: '2024-01-01T00:00:00.000Z',
103
- };
104
-
105
- expect(getDaysUntilExpiration(status)).toBe(5);
106
- });
107
-
108
- it('should return 0 for past expiration', () => {
109
- const pastDate = new Date();
110
- pastDate.setDate(pastDate.getDate() - 5);
111
-
112
- const status = {
113
- isPremium: true,
114
- expiresAt: pastDate.toISOString(),
115
- productId: 'monthly',
116
- purchasedAt: '2024-01-01T00:00:00.000Z',
117
- customerId: 'customer123',
118
- syncedAt: '2024-01-01T00:00:00.000Z',
119
- };
120
-
121
- expect(getDaysUntilExpiration(status)).toBe(0);
122
- });
123
-
124
- it('should return 0 for today expiration', () => {
125
- const today = new Date();
126
- today.setHours(0, 0, 0, 0); // Start of today
127
-
128
- const status = {
129
- isPremium: true,
130
- expiresAt: today.toISOString(),
131
- productId: 'monthly',
132
- purchasedAt: '2024-01-01T00:00:00.000Z',
133
- customerId: 'customer123',
134
- syncedAt: '2024-01-01T00:00:00.000Z',
135
- };
136
-
137
- expect(getDaysUntilExpiration(status)).toBe(0);
138
- });
139
- });
140
- });
@@ -1,53 +0,0 @@
1
- /**
2
- * Date Validation Utilities
3
- * Utilities for validating and checking subscription dates
4
- *
5
- * Following SOLID, DRY, KISS principles:
6
- * - Single Responsibility: Only date validation logic
7
- * - DRY: No code duplication
8
- * - KISS: Simple, clear implementations
9
- */
10
-
11
- import type { SubscriptionStatus } from '../domain/entities/SubscriptionStatus';
12
-
13
- /**
14
- * Check if subscription is expired
15
- */
16
- export function isSubscriptionExpired(
17
- status: SubscriptionStatus | null,
18
- ): boolean {
19
- if (!status || !status.isPremium) {
20
- return true;
21
- }
22
-
23
- if (!status.expiresAt) {
24
- // Lifetime subscription (no expiration)
25
- return false;
26
- }
27
-
28
- const expirationDate = new Date(status.expiresAt);
29
- const now = new Date();
30
-
31
- return expirationDate.getTime() <= now.getTime();
32
- }
33
-
34
- /**
35
- * Get days until subscription expires
36
- * Returns null for lifetime subscriptions
37
- */
38
- export function getDaysUntilExpiration(
39
- status: SubscriptionStatus | null,
40
- ): number | null {
41
- if (!status || !status.expiresAt) {
42
- return null;
43
- }
44
-
45
- const expirationDate = new Date(status.expiresAt);
46
- const now = new Date();
47
- const diffMs = expirationDate.getTime() - now.getTime();
48
- const diffDays = Math.ceil(
49
- diffMs / (1000 * 60 * 60 * 24),
50
- );
51
-
52
- return Math.max(0, diffDays);
53
- }