@umituz/react-native-subscription 2.14.90 → 2.14.92
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 +1 -1
- package/src/domain/entities/SubscriptionStatus.test.ts +20 -20
- package/src/domain/entities/SubscriptionStatus.ts +7 -7
- package/src/presentation/hooks/useSubscriptionDetails.ts +7 -5
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +3 -6
- package/src/presentation/utils/subscriptionDateUtils.ts +10 -30
- package/src/utils/authUtils.ts +0 -46
- package/src/utils/index.ts +0 -1
- package/src/utils/premiumStatusUtils.ts +5 -36
- package/src/utils/tierUtils.ts +1 -58
- package/src/utils/validation.ts +1 -97
- package/src/utils/__tests__/dateValidationUtils.test.ts +0 -140
- package/src/utils/dateValidationUtils.ts +0 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.92",
|
|
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
|
|
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(
|
|
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
|
|
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() -
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 {
|
|
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
|
|
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 &&
|
|
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()
|
|
@@ -24,38 +24,18 @@ export const convertPurchasedAt = (purchasedAt: unknown): string | null => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Formats a date string
|
|
27
|
+
* Formats a date string to a simple DD.MM.YYYY format
|
|
28
28
|
*/
|
|
29
|
-
export const
|
|
30
|
-
dateStr: string | null,
|
|
31
|
-
locale: string
|
|
32
|
-
): string | null => {
|
|
29
|
+
export const formatDate = (dateStr: string | null): string | null => {
|
|
33
30
|
if (!dateStr) return null;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return timezoneService.formatDate(new Date(dateStr), locale, {
|
|
37
|
-
year: "numeric",
|
|
38
|
-
month: "long",
|
|
39
|
-
day: "numeric",
|
|
40
|
-
});
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
};
|
|
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();
|
|
31
|
+
const date = new Date(dateStr);
|
|
32
|
+
if (isNaN(date.getTime())) return null;
|
|
56
33
|
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
34
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
35
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
36
|
+
const y = date.getFullYear();
|
|
37
|
+
|
|
38
|
+
return `${d}.${m}.${y}`;
|
|
60
39
|
};
|
|
61
40
|
|
|
41
|
+
|
package/src/utils/authUtils.ts
CHANGED
|
@@ -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
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
49
|
-
|
|
20
|
+
// Sync mode: return the provided isPremium value
|
|
21
|
+
if (typeof isPremiumOrFetcher === 'boolean') return isPremiumOrFetcher;
|
|
50
22
|
|
|
51
|
-
//
|
|
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
|
);
|
package/src/utils/tierUtils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/utils/validation.ts
CHANGED
|
@@ -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
|
-
}
|