@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.
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/lib/application/ports/ISubscriptionRepository.d.ts +25 -0
- package/lib/application/ports/ISubscriptionRepository.d.ts.map +1 -0
- package/lib/application/ports/ISubscriptionRepository.js +9 -0
- package/lib/application/ports/ISubscriptionRepository.js.map +1 -0
- package/lib/application/ports/ISubscriptionService.d.ts +28 -0
- package/lib/application/ports/ISubscriptionService.d.ts.map +1 -0
- package/lib/application/ports/ISubscriptionService.js +6 -0
- package/lib/application/ports/ISubscriptionService.js.map +1 -0
- package/lib/domain/entities/SubscriptionStatus.d.ts +31 -0
- package/lib/domain/entities/SubscriptionStatus.d.ts.map +1 -0
- package/lib/domain/entities/SubscriptionStatus.js +39 -0
- package/lib/domain/entities/SubscriptionStatus.js.map +1 -0
- package/lib/domain/errors/SubscriptionError.d.ts +18 -0
- package/lib/domain/errors/SubscriptionError.d.ts.map +1 -0
- package/lib/domain/errors/SubscriptionError.js +30 -0
- package/lib/domain/errors/SubscriptionError.js.map +1 -0
- package/lib/domain/value-objects/SubscriptionConfig.d.ts +15 -0
- package/lib/domain/value-objects/SubscriptionConfig.d.ts.map +1 -0
- package/lib/domain/value-objects/SubscriptionConfig.js +6 -0
- package/lib/domain/value-objects/SubscriptionConfig.js.map +1 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/infrastructure/services/ActivationHandler.d.ts +20 -0
- package/lib/infrastructure/services/ActivationHandler.d.ts.map +1 -0
- package/lib/infrastructure/services/ActivationHandler.js +71 -0
- package/lib/infrastructure/services/ActivationHandler.js.map +1 -0
- package/lib/infrastructure/services/SubscriptionService.d.ts +22 -0
- package/lib/infrastructure/services/SubscriptionService.d.ts.map +1 -0
- package/lib/infrastructure/services/SubscriptionService.js +110 -0
- package/lib/infrastructure/services/SubscriptionService.js.map +1 -0
- package/lib/presentation/hooks/useSubscription.d.ts +33 -0
- package/lib/presentation/hooks/useSubscription.d.ts.map +1 -0
- package/lib/presentation/hooks/useSubscription.js +129 -0
- package/lib/presentation/hooks/useSubscription.js.map +1 -0
- package/lib/utils/dateUtils.d.ts +39 -0
- package/lib/utils/dateUtils.d.ts.map +1 -0
- package/lib/utils/dateUtils.js +117 -0
- package/lib/utils/dateUtils.js.map +1 -0
- package/lib/utils/dateValidationUtils.d.ts +20 -0
- package/lib/utils/dateValidationUtils.d.ts.map +1 -0
- package/lib/utils/dateValidationUtils.js +39 -0
- package/lib/utils/dateValidationUtils.js.map +1 -0
- package/lib/utils/periodUtils.d.ts +38 -0
- package/lib/utils/periodUtils.d.ts.map +1 -0
- package/lib/utils/periodUtils.js +70 -0
- package/lib/utils/periodUtils.js.map +1 -0
- package/lib/utils/planDetectionUtils.d.ts +17 -0
- package/lib/utils/planDetectionUtils.d.ts.map +1 -0
- package/lib/utils/planDetectionUtils.js +31 -0
- package/lib/utils/planDetectionUtils.js.map +1 -0
- package/lib/utils/priceUtils.d.ts +23 -0
- package/lib/utils/priceUtils.d.ts.map +1 -0
- package/lib/utils/priceUtils.js +29 -0
- package/lib/utils/priceUtils.js.map +1 -0
- package/lib/utils/subscriptionConstants.d.ts +62 -0
- package/lib/utils/subscriptionConstants.d.ts.map +1 -0
- package/lib/utils/subscriptionConstants.js +61 -0
- package/lib/utils/subscriptionConstants.js.map +1 -0
- package/package.json +13 -3
- package/src/application/ports/ISubscriptionRepository.ts +0 -0
- package/src/application/ports/ISubscriptionService.ts +0 -0
- package/src/domain/entities/SubscriptionStatus.test.ts +106 -0
- package/src/domain/entities/SubscriptionStatus.ts +0 -0
- package/src/domain/errors/SubscriptionError.ts +0 -0
- package/src/domain/value-objects/SubscriptionConfig.ts +0 -0
- package/src/index.ts +9 -2
- package/src/infrastructure/services/ActivationHandler.ts +8 -0
- package/src/infrastructure/services/SubscriptionService.ts +13 -1
- package/src/presentation/hooks/useSubscription.ts +22 -2
- package/src/utils/dateUtils.test.ts +116 -0
- package/src/utils/dateUtils.ts +12 -76
- package/src/utils/dateValidationUtils.test.ts +142 -0
- package/src/utils/dateValidationUtils.ts +53 -0
- package/src/utils/periodUtils.ts +0 -0
- package/src/utils/planDetectionUtils.test.ts +47 -0
- package/src/utils/planDetectionUtils.ts +40 -0
- package/src/utils/priceUtils.test.ts +35 -0
- package/src/utils/priceUtils.ts +0 -0
- package/src/utils/subscriptionConstants.ts +0 -0
package/src/utils/dateUtils.ts
CHANGED
|
@@ -3,88 +3,20 @@
|
|
|
3
3
|
* Subscription date-related helper functions
|
|
4
4
|
*
|
|
5
5
|
* Following SOLID, DRY, KISS principles:
|
|
6
|
-
* - Single Responsibility: Only date
|
|
6
|
+
* - Single Responsibility: Only date formatting and calculation
|
|
7
7
|
* - DRY: No code duplication
|
|
8
8
|
* - KISS: Simple, clear implementations
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import { DATE_CONSTANTS } from './subscriptionConstants';
|
|
12
|
+
import { extractPlanFromProductId } from './planDetectionUtils';
|
|
12
13
|
import {
|
|
13
14
|
SUBSCRIPTION_PLAN_TYPES,
|
|
14
15
|
MIN_SUBSCRIPTION_DURATIONS_DAYS,
|
|
15
16
|
SUBSCRIPTION_PERIOD_DAYS,
|
|
16
|
-
DATE_CONSTANTS,
|
|
17
|
-
PRODUCT_ID_KEYWORDS,
|
|
18
17
|
type SubscriptionPlanType,
|
|
19
18
|
} from './subscriptionConstants';
|
|
20
19
|
|
|
21
|
-
/**
|
|
22
|
-
* Extract subscription plan type from product ID
|
|
23
|
-
* Example: "com.umituz.app.weekly" → "weekly"
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
26
|
-
function extractPlanFromProductId(
|
|
27
|
-
productId: string | null | undefined,
|
|
28
|
-
): SubscriptionPlanType {
|
|
29
|
-
if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
30
|
-
|
|
31
|
-
const lower = productId.toLowerCase();
|
|
32
|
-
|
|
33
|
-
if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
|
|
34
|
-
return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
|
|
35
|
-
}
|
|
36
|
-
if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
|
|
37
|
-
return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
|
|
38
|
-
}
|
|
39
|
-
if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
|
|
40
|
-
return SUBSCRIPTION_PLAN_TYPES.YEARLY;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Check if subscription is expired
|
|
48
|
-
*/
|
|
49
|
-
export function isSubscriptionExpired(
|
|
50
|
-
status: SubscriptionStatus | null,
|
|
51
|
-
): boolean {
|
|
52
|
-
if (!status || !status.isPremium) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!status.expiresAt) {
|
|
57
|
-
// Lifetime subscription (no expiration)
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const expirationDate = new Date(status.expiresAt);
|
|
62
|
-
const now = new Date();
|
|
63
|
-
|
|
64
|
-
return expirationDate.getTime() <= now.getTime();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get days until subscription expires
|
|
69
|
-
* Returns null for lifetime subscriptions
|
|
70
|
-
*/
|
|
71
|
-
export function getDaysUntilExpiration(
|
|
72
|
-
status: SubscriptionStatus | null,
|
|
73
|
-
): number | null {
|
|
74
|
-
if (!status || !status.expiresAt) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const expirationDate = new Date(status.expiresAt);
|
|
79
|
-
const now = new Date();
|
|
80
|
-
const diffMs = expirationDate.getTime() - now.getTime();
|
|
81
|
-
const diffDays = Math.ceil(
|
|
82
|
-
diffMs / DATE_CONSTANTS.MILLISECONDS_PER_DAY,
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return diffDays > 0 ? diffDays : 0;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
20
|
/**
|
|
89
21
|
* Format expiration date for display
|
|
90
22
|
*/
|
|
@@ -98,6 +30,9 @@ export function formatExpirationDate(
|
|
|
98
30
|
|
|
99
31
|
try {
|
|
100
32
|
const date = new Date(expiresAt);
|
|
33
|
+
if (isNaN(date.getTime())) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
101
36
|
return date.toLocaleDateString(locale, {
|
|
102
37
|
year: 'numeric',
|
|
103
38
|
month: 'long',
|
|
@@ -118,24 +53,26 @@ export function formatExpirationDate(
|
|
|
118
53
|
* - Yearly subscriptions: Same day next year (e.g., Nov 10, 2024 → Nov 10, 2025)
|
|
119
54
|
* - Weekly subscriptions: +7 days
|
|
120
55
|
*
|
|
121
|
-
* @param productId - Product identifier (e.g., "com.
|
|
56
|
+
* @param productId - Product identifier (e.g., "com.company.app.monthly")
|
|
122
57
|
* @param revenueCatExpiresAt - Optional expiration date from RevenueCat API
|
|
123
58
|
* @returns ISO date string for expiration, or null if invalid
|
|
124
59
|
*
|
|
125
60
|
* @example
|
|
126
61
|
* // Monthly subscription purchased on Nov 10, 2024
|
|
127
|
-
* calculateExpirationDate('com.
|
|
62
|
+
* calculateExpirationDate('com.company.app.monthly', null)
|
|
128
63
|
* // Returns: '2024-12-10T...' (Dec 10, 2024)
|
|
129
64
|
*
|
|
130
65
|
* @example
|
|
131
66
|
* // Yearly subscription purchased on Nov 10, 2024
|
|
132
|
-
* calculateExpirationDate('com.
|
|
67
|
+
* calculateExpirationDate('com.company.app.yearly', null)
|
|
133
68
|
* // Returns: '2025-11-10T...' (Nov 10, 2025)
|
|
134
69
|
*/
|
|
135
70
|
export function calculateExpirationDate(
|
|
136
71
|
productId: string | null | undefined,
|
|
137
72
|
revenueCatExpiresAt?: string | null,
|
|
138
73
|
): string | null {
|
|
74
|
+
if (!productId) return null;
|
|
75
|
+
|
|
139
76
|
const plan = extractPlanFromProductId(productId);
|
|
140
77
|
const now = new Date();
|
|
141
78
|
|
|
@@ -207,5 +144,4 @@ export function calculateExpirationDate(
|
|
|
207
144
|
}
|
|
208
145
|
|
|
209
146
|
return calculatedDate.toISOString();
|
|
210
|
-
}
|
|
211
|
-
|
|
147
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Date Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
isSubscriptionExpired,
|
|
7
|
+
getDaysUntilExpiration,
|
|
8
|
+
} from '../utils/dateValidationUtils';
|
|
9
|
+
|
|
10
|
+
describe('Date Validation Utils', () => {
|
|
11
|
+
describe('isSubscriptionExpired', () => {
|
|
12
|
+
it('should return true for null status', () => {
|
|
13
|
+
expect(isSubscriptionExpired(null)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true for non-premium status', () => {
|
|
17
|
+
const status = {
|
|
18
|
+
isPremium: false,
|
|
19
|
+
expiresAt: null,
|
|
20
|
+
productId: null,
|
|
21
|
+
purchasedAt: null,
|
|
22
|
+
customerId: null,
|
|
23
|
+
syncedAt: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(isSubscriptionExpired(status)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return false for lifetime subscription', () => {
|
|
30
|
+
const status = {
|
|
31
|
+
isPremium: true,
|
|
32
|
+
expiresAt: null,
|
|
33
|
+
productId: 'lifetime',
|
|
34
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
35
|
+
customerId: 'customer123',
|
|
36
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(isSubscriptionExpired(status)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return false for future expiration', () => {
|
|
43
|
+
const futureDate = new Date();
|
|
44
|
+
futureDate.setDate(futureDate.getDate() + 30);
|
|
45
|
+
|
|
46
|
+
const status = {
|
|
47
|
+
isPremium: true,
|
|
48
|
+
expiresAt: futureDate.toISOString(),
|
|
49
|
+
productId: 'monthly',
|
|
50
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
51
|
+
customerId: 'customer123',
|
|
52
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(isSubscriptionExpired(status)).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return true for past expiration', () => {
|
|
59
|
+
const pastDate = new Date();
|
|
60
|
+
pastDate.setDate(pastDate.getDate() - 1);
|
|
61
|
+
|
|
62
|
+
const status = {
|
|
63
|
+
isPremium: true,
|
|
64
|
+
expiresAt: pastDate.toISOString(),
|
|
65
|
+
productId: 'monthly',
|
|
66
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
67
|
+
customerId: 'customer123',
|
|
68
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = getDaysUntilExpiration(status);
|
|
72
|
+
expect(result === 0 || result === -0).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('getDaysUntilExpiration', () => {
|
|
77
|
+
it('should return null for null status', () => {
|
|
78
|
+
expect(getDaysUntilExpiration(null)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return null for status without expiration', () => {
|
|
82
|
+
const status = {
|
|
83
|
+
isPremium: true,
|
|
84
|
+
expiresAt: null,
|
|
85
|
+
productId: 'lifetime',
|
|
86
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
87
|
+
customerId: 'customer123',
|
|
88
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
expect(getDaysUntilExpiration(status)).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return positive days for future expiration', () => {
|
|
95
|
+
const futureDate = new Date();
|
|
96
|
+
futureDate.setDate(futureDate.getDate() + 5);
|
|
97
|
+
|
|
98
|
+
const status = {
|
|
99
|
+
isPremium: true,
|
|
100
|
+
expiresAt: futureDate.toISOString(),
|
|
101
|
+
productId: 'monthly',
|
|
102
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
103
|
+
customerId: 'customer123',
|
|
104
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(getDaysUntilExpiration(status)).toBe(5);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return 0 for past expiration', () => {
|
|
111
|
+
const pastDate = new Date();
|
|
112
|
+
pastDate.setDate(pastDate.getDate() - 5);
|
|
113
|
+
|
|
114
|
+
const status = {
|
|
115
|
+
isPremium: true,
|
|
116
|
+
expiresAt: pastDate.toISOString(),
|
|
117
|
+
productId: 'monthly',
|
|
118
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
119
|
+
customerId: 'customer123',
|
|
120
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(getDaysUntilExpiration(status)).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return 0 for today expiration', () => {
|
|
127
|
+
const today = new Date();
|
|
128
|
+
today.setHours(0, 0, 0, 0); // Start of today
|
|
129
|
+
|
|
130
|
+
const status = {
|
|
131
|
+
isPremium: true,
|
|
132
|
+
expiresAt: today.toISOString(),
|
|
133
|
+
productId: 'monthly',
|
|
134
|
+
purchasedAt: '2024-01-01T00:00:00.000Z',
|
|
135
|
+
customerId: 'customer123',
|
|
136
|
+
syncedAt: '2024-01-01T00:00:00.000Z',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
expect(getDaysUntilExpiration(status)).toBe(0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
}
|
package/src/utils/periodUtils.ts
CHANGED
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Plan Detection Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { extractPlanFromProductId } from '../utils/planDetectionUtils';
|
|
6
|
+
import { SUBSCRIPTION_PLAN_TYPES } from '../utils/subscriptionConstants';
|
|
7
|
+
|
|
8
|
+
describe('Plan Detection Utils', () => {
|
|
9
|
+
describe('extractPlanFromProductId', () => {
|
|
10
|
+
it('should return UNKNOWN for null/undefined/empty productId', () => {
|
|
11
|
+
expect(extractPlanFromProductId(null)).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
12
|
+
expect(extractPlanFromProductId(undefined)).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
13
|
+
expect(extractPlanFromProductId('')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should detect weekly plans', () => {
|
|
17
|
+
expect(extractPlanFromProductId('com.app.weekly')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
|
|
18
|
+
expect(extractPlanFromProductId('com.app.week')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
|
|
19
|
+
expect(extractPlanFromProductId('WEEKLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should detect monthly plans', () => {
|
|
23
|
+
expect(extractPlanFromProductId('com.app.monthly')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
|
|
24
|
+
expect(extractPlanFromProductId('com.app.month')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
|
|
25
|
+
expect(extractPlanFromProductId('MONTHLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should detect yearly plans', () => {
|
|
29
|
+
expect(extractPlanFromProductId('com.app.yearly')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
|
|
30
|
+
expect(extractPlanFromProductId('com.app.year')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
|
|
31
|
+
expect(extractPlanFromProductId('com.app.annual')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
|
|
32
|
+
expect(extractPlanFromProductId('YEARLY_PREMIUM')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should be case insensitive', () => {
|
|
36
|
+
expect(extractPlanFromProductId('WEEKLY')).toBe(SUBSCRIPTION_PLAN_TYPES.WEEKLY);
|
|
37
|
+
expect(extractPlanFromProductId('Monthly')).toBe(SUBSCRIPTION_PLAN_TYPES.MONTHLY);
|
|
38
|
+
expect(extractPlanFromProductId('YEARLY')).toBe(SUBSCRIPTION_PLAN_TYPES.YEARLY);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return UNKNOWN for unrecognized patterns', () => {
|
|
42
|
+
expect(extractPlanFromProductId('com.app.lifetime')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
43
|
+
expect(extractPlanFromProductId('com.app.premium')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
44
|
+
expect(extractPlanFromProductId('random_string')).toBe(SUBSCRIPTION_PLAN_TYPES.UNKNOWN);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Detection Utilities
|
|
3
|
+
* Utilities for detecting subscription plan types from product IDs
|
|
4
|
+
*
|
|
5
|
+
* Following SOLID, DRY, KISS principles:
|
|
6
|
+
* - Single Responsibility: Only plan detection logic
|
|
7
|
+
* - DRY: No code duplication
|
|
8
|
+
* - KISS: Simple, clear implementations
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
SUBSCRIPTION_PLAN_TYPES,
|
|
13
|
+
PRODUCT_ID_KEYWORDS,
|
|
14
|
+
type SubscriptionPlanType,
|
|
15
|
+
} from './subscriptionConstants';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract subscription plan type from product ID
|
|
19
|
+
* Example: "com.company.app.weekly" → "weekly"
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export function extractPlanFromProductId(
|
|
23
|
+
productId: string | null | undefined,
|
|
24
|
+
): SubscriptionPlanType {
|
|
25
|
+
if (!productId) return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
26
|
+
|
|
27
|
+
const lower = productId.toLowerCase();
|
|
28
|
+
|
|
29
|
+
if (PRODUCT_ID_KEYWORDS.WEEKLY.some((keyword) => lower.includes(keyword))) {
|
|
30
|
+
return SUBSCRIPTION_PLAN_TYPES.WEEKLY;
|
|
31
|
+
}
|
|
32
|
+
if (PRODUCT_ID_KEYWORDS.MONTHLY.some((keyword) => lower.includes(keyword))) {
|
|
33
|
+
return SUBSCRIPTION_PLAN_TYPES.MONTHLY;
|
|
34
|
+
}
|
|
35
|
+
if (PRODUCT_ID_KEYWORDS.YEARLY.some((keyword) => lower.includes(keyword))) {
|
|
36
|
+
return SUBSCRIPTION_PLAN_TYPES.YEARLY;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return SUBSCRIPTION_PLAN_TYPES.UNKNOWN;
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Price Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { formatPrice } from '../utils/priceUtils';
|
|
6
|
+
|
|
7
|
+
describe('Price Utils', () => {
|
|
8
|
+
describe('formatPrice', () => {
|
|
9
|
+
it('should format USD price', () => {
|
|
10
|
+
expect(formatPrice(9.99, 'USD')).toBe('$9.99');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should format EUR price', () => {
|
|
14
|
+
expect(formatPrice(19.99, 'EUR')).toMatch(/€19\.99/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should format TRY price', () => {
|
|
18
|
+
const result = formatPrice(229.99, 'TRY');
|
|
19
|
+
expect(result).toBeTruthy();
|
|
20
|
+
expect(result).toContain('229.99');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should format whole numbers', () => {
|
|
24
|
+
expect(formatPrice(10, 'USD')).toBe('$10.00');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should handle zero price', () => {
|
|
28
|
+
expect(formatPrice(0, 'USD')).toBe('$0.00');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should format large numbers', () => {
|
|
32
|
+
expect(formatPrice(999.99, 'USD')).toBe('$999.99');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/utils/priceUtils.ts
CHANGED
|
File without changes
|
|
File without changes
|