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