@umituz/react-native-subscription 1.0.6 → 1.0.7
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/package.json +1 -1
- package/src/application/ports/ISubscriptionRepository.ts +10 -0
- package/src/application/ports/ISubscriptionService.ts +10 -0
- package/src/domain/entities/SubscriptionStatus.ts +10 -0
- package/src/domain/errors/SubscriptionError.ts +10 -0
- package/src/infrastructure/services/ActivationHandler.ts +100 -0
- package/src/infrastructure/services/SubscriptionService.ts +48 -179
package/LICENSE
CHANGED
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Subscription management system for React Native apps - Database-first approach with secure validation",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
const updatedStatus = await config.repository.updateSubscriptionStatus(
|
|
30
|
+
userId,
|
|
31
|
+
{
|
|
32
|
+
isPremium: true,
|
|
33
|
+
productId,
|
|
34
|
+
expiresAt,
|
|
35
|
+
purchasedAt: new Date().toISOString(),
|
|
36
|
+
syncedAt: new Date().toISOString(),
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
await notifyStatusChange(config, userId, updatedStatus);
|
|
41
|
+
return updatedStatus;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await handleError(config, error, "activateSubscription");
|
|
44
|
+
throw new SubscriptionRepositoryError("Failed to activate subscription");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deactivate subscription for user
|
|
50
|
+
*/
|
|
51
|
+
export async function deactivateSubscription(
|
|
52
|
+
config: ActivationHandlerConfig,
|
|
53
|
+
userId: string
|
|
54
|
+
): Promise<SubscriptionStatus> {
|
|
55
|
+
try {
|
|
56
|
+
const updatedStatus = await config.repository.updateSubscriptionStatus(
|
|
57
|
+
userId,
|
|
58
|
+
{
|
|
59
|
+
isPremium: false,
|
|
60
|
+
expiresAt: null,
|
|
61
|
+
productId: null,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
await notifyStatusChange(config, userId, updatedStatus);
|
|
66
|
+
return updatedStatus;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await handleError(config, error, "deactivateSubscription");
|
|
69
|
+
throw new SubscriptionRepositoryError("Failed to deactivate subscription");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function notifyStatusChange(
|
|
74
|
+
config: ActivationHandlerConfig,
|
|
75
|
+
userId: string,
|
|
76
|
+
status: SubscriptionStatus
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (!config.onStatusChanged) return;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await config.onStatusChanged(userId, status);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await handleError(config, error, "onStatusChanged");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleError(
|
|
88
|
+
config: ActivationHandlerConfig,
|
|
89
|
+
error: unknown,
|
|
90
|
+
context: string
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
if (!config.onError) return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
96
|
+
await config.onError(err, `ActivationHandler.${context}`);
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore callback errors
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,51 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subscription Service Implementation
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* SECURITY: Database-first approach ensures:
|
|
6
|
-
* - 10-50x faster subscription checks
|
|
7
|
-
* - Works offline (database cache)
|
|
8
|
-
* - More reliable than SDK-dependent checks
|
|
9
|
-
* - Server-side validation always enforced
|
|
3
|
+
* Database-first subscription management
|
|
10
4
|
*/
|
|
11
5
|
|
|
12
|
-
import type { ISubscriptionService } from
|
|
13
|
-
import type { ISubscriptionRepository } from
|
|
14
|
-
import type { SubscriptionStatus } from
|
|
15
|
-
import {
|
|
16
|
-
createDefaultSubscriptionStatus,
|
|
17
|
-
isSubscriptionValid,
|
|
18
|
-
} from '../../domain/entities/SubscriptionStatus';
|
|
6
|
+
import type { ISubscriptionService } from "../../application/ports/ISubscriptionService";
|
|
7
|
+
import type { ISubscriptionRepository } from "../../application/ports/ISubscriptionRepository";
|
|
8
|
+
import type { SubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
|
|
9
|
+
import { createDefaultSubscriptionStatus } from "../../domain/entities/SubscriptionStatus";
|
|
19
10
|
import {
|
|
20
11
|
SubscriptionRepositoryError,
|
|
21
12
|
SubscriptionValidationError,
|
|
22
|
-
} from
|
|
23
|
-
import type { SubscriptionConfig } from
|
|
13
|
+
} from "../../domain/errors/SubscriptionError";
|
|
14
|
+
import type { SubscriptionConfig } from "../../domain/value-objects/SubscriptionConfig";
|
|
15
|
+
import {
|
|
16
|
+
activateSubscription,
|
|
17
|
+
deactivateSubscription,
|
|
18
|
+
type ActivationHandlerConfig,
|
|
19
|
+
} from "./ActivationHandler";
|
|
24
20
|
|
|
25
21
|
export class SubscriptionService implements ISubscriptionService {
|
|
26
22
|
private repository: ISubscriptionRepository;
|
|
27
|
-
private
|
|
28
|
-
userId: string,
|
|
29
|
-
status: SubscriptionStatus,
|
|
30
|
-
) => Promise<void> | void;
|
|
31
|
-
private onError?: (error: Error, context: string) => Promise<void> | void;
|
|
23
|
+
private handlerConfig: ActivationHandlerConfig;
|
|
32
24
|
|
|
33
25
|
constructor(config: SubscriptionConfig) {
|
|
34
26
|
if (!config.repository) {
|
|
35
|
-
throw new SubscriptionValidationError(
|
|
36
|
-
'Repository is required for SubscriptionService',
|
|
37
|
-
);
|
|
27
|
+
throw new SubscriptionValidationError("Repository is required");
|
|
38
28
|
}
|
|
39
29
|
|
|
40
30
|
this.repository = config.repository;
|
|
41
|
-
this.
|
|
42
|
-
|
|
31
|
+
this.handlerConfig = {
|
|
32
|
+
repository: config.repository,
|
|
33
|
+
onStatusChanged: config.onStatusChanged,
|
|
34
|
+
onError: config.onError,
|
|
35
|
+
};
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
/**
|
|
46
|
-
* Get subscription status for a user
|
|
47
|
-
* Returns default (free) status if user not found
|
|
48
|
-
*/
|
|
49
38
|
async getSubscriptionStatus(userId: string): Promise<SubscriptionStatus> {
|
|
50
39
|
try {
|
|
51
40
|
const status = await this.repository.getSubscriptionStatus(userId);
|
|
@@ -53,131 +42,45 @@ export class SubscriptionService implements ISubscriptionService {
|
|
|
53
42
|
return createDefaultSubscriptionStatus();
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
// Validate subscription status (check expiration)
|
|
57
45
|
const isValid = this.repository.isSubscriptionValid(status);
|
|
58
46
|
if (!isValid && status.isPremium) {
|
|
59
|
-
|
|
60
|
-
const updatedStatus = await this.deactivateSubscription(userId);
|
|
61
|
-
return updatedStatus;
|
|
47
|
+
return await this.deactivateSubscription(userId);
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
return status;
|
|
65
51
|
} catch (error) {
|
|
66
|
-
await this.handleError(
|
|
67
|
-
error instanceof Error
|
|
68
|
-
? error
|
|
69
|
-
: new Error('Error getting subscription status'),
|
|
70
|
-
'SubscriptionService.getSubscriptionStatus',
|
|
71
|
-
);
|
|
52
|
+
await this.handleError(error, "getSubscriptionStatus");
|
|
72
53
|
return createDefaultSubscriptionStatus();
|
|
73
54
|
}
|
|
74
55
|
}
|
|
75
56
|
|
|
76
|
-
/**
|
|
77
|
-
* Check if user has active subscription
|
|
78
|
-
*/
|
|
79
57
|
async isPremium(userId: string): Promise<boolean> {
|
|
80
58
|
const status = await this.getSubscriptionStatus(userId);
|
|
81
59
|
return this.repository.isSubscriptionValid(status);
|
|
82
60
|
}
|
|
83
61
|
|
|
84
|
-
/**
|
|
85
|
-
* Activate subscription
|
|
86
|
-
*/
|
|
87
62
|
async activateSubscription(
|
|
88
63
|
userId: string,
|
|
89
64
|
productId: string,
|
|
90
|
-
expiresAt: string | null
|
|
65
|
+
expiresAt: string | null
|
|
91
66
|
): Promise<SubscriptionStatus> {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expiresAt,
|
|
99
|
-
purchasedAt: new Date().toISOString(),
|
|
100
|
-
syncedAt: new Date().toISOString(),
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Call callback if provided
|
|
105
|
-
if (this.onStatusChanged) {
|
|
106
|
-
try {
|
|
107
|
-
await this.onStatusChanged(userId, updatedStatus);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
// Don't fail activation if callback fails
|
|
110
|
-
await this.handleError(
|
|
111
|
-
error instanceof Error ? error : new Error('Callback failed'),
|
|
112
|
-
'SubscriptionService.activateSubscription.onStatusChanged',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return updatedStatus;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
await this.handleError(
|
|
120
|
-
error instanceof Error
|
|
121
|
-
? error
|
|
122
|
-
: new Error('Error activating subscription'),
|
|
123
|
-
'SubscriptionService.activateSubscription',
|
|
124
|
-
);
|
|
125
|
-
throw new SubscriptionRepositoryError(
|
|
126
|
-
'Failed to activate subscription',
|
|
127
|
-
);
|
|
128
|
-
}
|
|
67
|
+
return activateSubscription(
|
|
68
|
+
this.handlerConfig,
|
|
69
|
+
userId,
|
|
70
|
+
productId,
|
|
71
|
+
expiresAt
|
|
72
|
+
);
|
|
129
73
|
}
|
|
130
74
|
|
|
131
|
-
/**
|
|
132
|
-
* Deactivate subscription
|
|
133
|
-
*/
|
|
134
75
|
async deactivateSubscription(userId: string): Promise<SubscriptionStatus> {
|
|
135
|
-
|
|
136
|
-
const updatedStatus = await this.repository.updateSubscriptionStatus(
|
|
137
|
-
userId,
|
|
138
|
-
{
|
|
139
|
-
isPremium: false,
|
|
140
|
-
expiresAt: null,
|
|
141
|
-
productId: null,
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Call callback if provided
|
|
146
|
-
if (this.onStatusChanged) {
|
|
147
|
-
try {
|
|
148
|
-
await this.onStatusChanged(userId, updatedStatus);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
// Don't fail deactivation if callback fails
|
|
151
|
-
await this.handleError(
|
|
152
|
-
error instanceof Error ? error : new Error('Callback failed'),
|
|
153
|
-
'SubscriptionService.deactivateSubscription.onStatusChanged',
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return updatedStatus;
|
|
159
|
-
} catch (error) {
|
|
160
|
-
await this.handleError(
|
|
161
|
-
error instanceof Error
|
|
162
|
-
? error
|
|
163
|
-
: new Error('Error deactivating subscription'),
|
|
164
|
-
'SubscriptionService.deactivateSubscription',
|
|
165
|
-
);
|
|
166
|
-
throw new SubscriptionRepositoryError(
|
|
167
|
-
'Failed to deactivate subscription',
|
|
168
|
-
);
|
|
169
|
-
}
|
|
76
|
+
return deactivateSubscription(this.handlerConfig, userId);
|
|
170
77
|
}
|
|
171
78
|
|
|
172
|
-
/**
|
|
173
|
-
* Update subscription status
|
|
174
|
-
*/
|
|
175
79
|
async updateSubscriptionStatus(
|
|
176
80
|
userId: string,
|
|
177
|
-
updates: Partial<SubscriptionStatus
|
|
81
|
+
updates: Partial<SubscriptionStatus>
|
|
178
82
|
): Promise<SubscriptionStatus> {
|
|
179
83
|
try {
|
|
180
|
-
// Add syncedAt timestamp
|
|
181
84
|
const updatesWithSync = {
|
|
182
85
|
...updates,
|
|
183
86
|
syncedAt: new Date().toISOString(),
|
|
@@ -185,61 +88,40 @@ export class SubscriptionService implements ISubscriptionService {
|
|
|
185
88
|
|
|
186
89
|
const updatedStatus = await this.repository.updateSubscriptionStatus(
|
|
187
90
|
userId,
|
|
188
|
-
updatesWithSync
|
|
91
|
+
updatesWithSync
|
|
189
92
|
);
|
|
190
93
|
|
|
191
|
-
|
|
192
|
-
if (this.onStatusChanged) {
|
|
94
|
+
if (this.handlerConfig.onStatusChanged) {
|
|
193
95
|
try {
|
|
194
|
-
await this.onStatusChanged(userId, updatedStatus);
|
|
96
|
+
await this.handlerConfig.onStatusChanged(userId, updatedStatus);
|
|
195
97
|
} catch (error) {
|
|
196
|
-
|
|
197
|
-
await this.handleError(
|
|
198
|
-
error instanceof Error ? error : new Error('Callback failed'),
|
|
199
|
-
'SubscriptionService.updateSubscriptionStatus.onStatusChanged',
|
|
200
|
-
);
|
|
98
|
+
await this.handleError(error, "updateSubscriptionStatus.callback");
|
|
201
99
|
}
|
|
202
100
|
}
|
|
203
101
|
|
|
204
102
|
return updatedStatus;
|
|
205
103
|
} catch (error) {
|
|
206
|
-
await this.handleError(
|
|
207
|
-
|
|
208
|
-
? error
|
|
209
|
-
: new Error('Error updating subscription status'),
|
|
210
|
-
'SubscriptionService.updateSubscriptionStatus',
|
|
211
|
-
);
|
|
212
|
-
throw new SubscriptionRepositoryError(
|
|
213
|
-
'Failed to update subscription status',
|
|
214
|
-
);
|
|
104
|
+
await this.handleError(error, "updateSubscriptionStatus");
|
|
105
|
+
throw new SubscriptionRepositoryError("Failed to update subscription");
|
|
215
106
|
}
|
|
216
107
|
}
|
|
217
108
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Ignore callback errors
|
|
227
|
-
}
|
|
109
|
+
private async handleError(error: unknown, context: string): Promise<void> {
|
|
110
|
+
if (!this.handlerConfig.onError) return;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
114
|
+
await this.handlerConfig.onError(err, `SubscriptionService.${context}`);
|
|
115
|
+
} catch {
|
|
116
|
+
// Ignore callback errors
|
|
228
117
|
}
|
|
229
118
|
}
|
|
230
119
|
}
|
|
231
120
|
|
|
232
|
-
/**
|
|
233
|
-
* Singleton instance
|
|
234
|
-
* Apps should use initializeSubscriptionService() to set up with their config
|
|
235
|
-
*/
|
|
236
121
|
let subscriptionServiceInstance: SubscriptionService | null = null;
|
|
237
122
|
|
|
238
|
-
/**
|
|
239
|
-
* Initialize Subscription service with configuration
|
|
240
|
-
*/
|
|
241
123
|
export function initializeSubscriptionService(
|
|
242
|
-
config: SubscriptionConfig
|
|
124
|
+
config: SubscriptionConfig
|
|
243
125
|
): SubscriptionService {
|
|
244
126
|
if (!subscriptionServiceInstance) {
|
|
245
127
|
subscriptionServiceInstance = new SubscriptionService(config);
|
|
@@ -247,27 +129,14 @@ export function initializeSubscriptionService(
|
|
|
247
129
|
return subscriptionServiceInstance;
|
|
248
130
|
}
|
|
249
131
|
|
|
250
|
-
/**
|
|
251
|
-
* Get Subscription service instance
|
|
252
|
-
* Returns null if service is not initialized (graceful degradation)
|
|
253
|
-
*/
|
|
254
132
|
export function getSubscriptionService(): SubscriptionService | null {
|
|
255
|
-
if (!subscriptionServiceInstance) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
console.warn(
|
|
259
|
-
'Subscription service is not initialized. Call initializeSubscriptionService() first.',
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
return null;
|
|
133
|
+
if (!subscriptionServiceInstance && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
134
|
+
// eslint-disable-next-line no-console
|
|
135
|
+
console.warn("[Subscription] Service not initialized");
|
|
263
136
|
}
|
|
264
137
|
return subscriptionServiceInstance;
|
|
265
138
|
}
|
|
266
139
|
|
|
267
|
-
/**
|
|
268
|
-
* Reset Subscription service (useful for testing)
|
|
269
|
-
*/
|
|
270
140
|
export function resetSubscriptionService(): void {
|
|
271
141
|
subscriptionServiceInstance = null;
|
|
272
142
|
}
|
|
273
|
-
|