@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,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.0
|
|
3
|
+
"version": "1.1.0",
|
|
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"
|
|
@@ -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
|
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
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activation Handler
|
|
3
|
+
* Handles subscription activation and deactivation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ISubscriptionRepository } from "../../application/ports/ISubscriptionRepository";
|
|
7
|
+
import type { SubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
|
|
8
|
+
import { SubscriptionRepositoryError } from "../../domain/errors/SubscriptionError";
|
|
9
|
+
|
|
10
|
+
export interface ActivationHandlerConfig {
|
|
11
|
+
repository: ISubscriptionRepository;
|
|
12
|
+
onStatusChanged?: (
|
|
13
|
+
userId: string,
|
|
14
|
+
status: SubscriptionStatus
|
|
15
|
+
) => Promise<void> | void;
|
|
16
|
+
onError?: (error: Error, context: string) => Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Activate subscription for user
|
|
21
|
+
*/
|
|
22
|
+
export async function activateSubscription(
|
|
23
|
+
config: ActivationHandlerConfig,
|
|
24
|
+
userId: string,
|
|
25
|
+
productId: string,
|
|
26
|
+
expiresAt: string | null
|
|
27
|
+
): Promise<SubscriptionStatus> {
|
|
28
|
+
try {
|
|
29
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
|
|
30
|
+
console.log("[Subscription] Activating subscription in handler", { userId, productId, expiresAt });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const updatedStatus = await config.repository.updateSubscriptionStatus(
|
|
34
|
+
userId,
|
|
35
|
+
{
|
|
36
|
+
isPremium: true,
|
|
37
|
+
productId,
|
|
38
|
+
expiresAt,
|
|
39
|
+
purchasedAt: new Date().toISOString(),
|
|
40
|
+
syncedAt: new Date().toISOString(),
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await notifyStatusChange(config, userId, updatedStatus);
|
|
45
|
+
return updatedStatus;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
await handleError(config, error, "activateSubscription");
|
|
48
|
+
throw new SubscriptionRepositoryError("Failed to activate subscription");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deactivate subscription for user
|
|
54
|
+
*/
|
|
55
|
+
export async function deactivateSubscription(
|
|
56
|
+
config: ActivationHandlerConfig,
|
|
57
|
+
userId: string
|
|
58
|
+
): Promise<SubscriptionStatus> {
|
|
59
|
+
try {
|
|
60
|
+
if (typeof globalThis !== 'undefined' && (globalThis as any).__DEV__) {
|
|
61
|
+
console.log("[Subscription] Deactivating subscription in handler", { userId });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const updatedStatus = await config.repository.updateSubscriptionStatus(
|
|
65
|
+
userId,
|
|
66
|
+
{
|
|
67
|
+
isPremium: false,
|
|
68
|
+
expiresAt: null,
|
|
69
|
+
productId: null,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await notifyStatusChange(config, userId, updatedStatus);
|
|
74
|
+
return updatedStatus;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
await handleError(config, error, "deactivateSubscription");
|
|
77
|
+
throw new SubscriptionRepositoryError("Failed to deactivate subscription");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function notifyStatusChange(
|
|
82
|
+
config: ActivationHandlerConfig,
|
|
83
|
+
userId: string,
|
|
84
|
+
status: SubscriptionStatus
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
if (!config.onStatusChanged) return;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await config.onStatusChanged(userId, status);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
await handleError(config, error, "onStatusChanged");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handleError(
|
|
96
|
+
config: ActivationHandlerConfig,
|
|
97
|
+
error: unknown,
|
|
98
|
+
context: string
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
if (!config.onError) return;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
104
|
+
await config.onError(err, `ActivationHandler.${context}`);
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore callback errors
|
|
107
|
+
}
|
|
108
|
+
}
|