@umituz/react-native-subscription 1.0.6 → 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 +10 -0
- package/README.md +10 -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 +10 -0
- package/src/application/ports/ISubscriptionService.ts +10 -0
- package/src/domain/entities/SubscriptionStatus.test.ts +106 -0
- package/src/domain/entities/SubscriptionStatus.ts +10 -0
- package/src/domain/errors/SubscriptionError.ts +10 -0
- package/src/domain/value-objects/SubscriptionConfig.ts +0 -0
- package/src/index.ts +9 -2
- package/src/infrastructure/services/ActivationHandler.ts +108 -0
- package/src/infrastructure/services/SubscriptionService.ts +58 -177
- 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
|
@@ -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
|