@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 CHANGED
@@ -20,3 +20,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
package/README.md CHANGED
@@ -204,3 +204,13 @@ initializeRevenueCatService({
204
204
 
205
205
  MIT
206
206
 
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "1.0.6",
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",
@@ -30,3 +30,13 @@ export interface ISubscriptionRepository {
30
30
  isSubscriptionValid(status: SubscriptionStatus): boolean;
31
31
  }
32
32
 
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
@@ -39,3 +39,13 @@ export interface ISubscriptionService {
39
39
  ): Promise<SubscriptionStatus>;
40
40
  }
41
41
 
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
@@ -62,3 +62,13 @@ export function isSubscriptionValid(status: SubscriptionStatus | null): boolean
62
62
  return expirationDate.getTime() > now.getTime() - bufferMs;
63
63
  }
64
64
 
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
@@ -31,3 +31,13 @@ export class SubscriptionConfigurationError extends SubscriptionError {
31
31
  }
32
32
  }
33
33
 
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
@@ -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
- * Secure subscription management with database-first approach
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 '../../application/ports/ISubscriptionService';
13
- import type { ISubscriptionRepository } from '../../application/ports/ISubscriptionRepository';
14
- import type { SubscriptionStatus } from '../../domain/entities/SubscriptionStatus';
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 '../../domain/errors/SubscriptionError';
23
- import type { SubscriptionConfig } from '../../domain/value-objects/SubscriptionConfig';
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 onStatusChanged?: (
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.onStatusChanged = config.onStatusChanged;
42
- this.onError = config.onError;
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
- // Subscription expired, update status
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
- try {
93
- const updatedStatus = await this.repository.updateSubscriptionStatus(
94
- userId,
95
- {
96
- isPremium: true,
97
- productId,
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
- try {
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
- // Call callback if provided
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
- // Don't fail update if callback fails
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
- error instanceof Error
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
- * Handle errors with optional callback
220
- */
221
- private async handleError(error: Error, context: string): Promise<void> {
222
- if (this.onError) {
223
- try {
224
- await this.onError(error, context);
225
- } catch {
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
- /* eslint-disable-next-line no-console */
257
- if (__DEV__) {
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
-