@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.
Files changed (83) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +10 -0
  3. package/lib/application/ports/ISubscriptionRepository.d.ts +25 -0
  4. package/lib/application/ports/ISubscriptionRepository.d.ts.map +1 -0
  5. package/lib/application/ports/ISubscriptionRepository.js +9 -0
  6. package/lib/application/ports/ISubscriptionRepository.js.map +1 -0
  7. package/lib/application/ports/ISubscriptionService.d.ts +28 -0
  8. package/lib/application/ports/ISubscriptionService.d.ts.map +1 -0
  9. package/lib/application/ports/ISubscriptionService.js +6 -0
  10. package/lib/application/ports/ISubscriptionService.js.map +1 -0
  11. package/lib/domain/entities/SubscriptionStatus.d.ts +31 -0
  12. package/lib/domain/entities/SubscriptionStatus.d.ts.map +1 -0
  13. package/lib/domain/entities/SubscriptionStatus.js +39 -0
  14. package/lib/domain/entities/SubscriptionStatus.js.map +1 -0
  15. package/lib/domain/errors/SubscriptionError.d.ts +18 -0
  16. package/lib/domain/errors/SubscriptionError.d.ts.map +1 -0
  17. package/lib/domain/errors/SubscriptionError.js +30 -0
  18. package/lib/domain/errors/SubscriptionError.js.map +1 -0
  19. package/lib/domain/value-objects/SubscriptionConfig.d.ts +15 -0
  20. package/lib/domain/value-objects/SubscriptionConfig.d.ts.map +1 -0
  21. package/lib/domain/value-objects/SubscriptionConfig.js +6 -0
  22. package/lib/domain/value-objects/SubscriptionConfig.js.map +1 -0
  23. package/lib/index.d.ts +33 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +43 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/infrastructure/services/ActivationHandler.d.ts +20 -0
  28. package/lib/infrastructure/services/ActivationHandler.d.ts.map +1 -0
  29. package/lib/infrastructure/services/ActivationHandler.js +71 -0
  30. package/lib/infrastructure/services/ActivationHandler.js.map +1 -0
  31. package/lib/infrastructure/services/SubscriptionService.d.ts +22 -0
  32. package/lib/infrastructure/services/SubscriptionService.d.ts.map +1 -0
  33. package/lib/infrastructure/services/SubscriptionService.js +110 -0
  34. package/lib/infrastructure/services/SubscriptionService.js.map +1 -0
  35. package/lib/presentation/hooks/useSubscription.d.ts +33 -0
  36. package/lib/presentation/hooks/useSubscription.d.ts.map +1 -0
  37. package/lib/presentation/hooks/useSubscription.js +129 -0
  38. package/lib/presentation/hooks/useSubscription.js.map +1 -0
  39. package/lib/utils/dateUtils.d.ts +39 -0
  40. package/lib/utils/dateUtils.d.ts.map +1 -0
  41. package/lib/utils/dateUtils.js +117 -0
  42. package/lib/utils/dateUtils.js.map +1 -0
  43. package/lib/utils/dateValidationUtils.d.ts +20 -0
  44. package/lib/utils/dateValidationUtils.d.ts.map +1 -0
  45. package/lib/utils/dateValidationUtils.js +39 -0
  46. package/lib/utils/dateValidationUtils.js.map +1 -0
  47. package/lib/utils/periodUtils.d.ts +38 -0
  48. package/lib/utils/periodUtils.d.ts.map +1 -0
  49. package/lib/utils/periodUtils.js +70 -0
  50. package/lib/utils/periodUtils.js.map +1 -0
  51. package/lib/utils/planDetectionUtils.d.ts +17 -0
  52. package/lib/utils/planDetectionUtils.d.ts.map +1 -0
  53. package/lib/utils/planDetectionUtils.js +31 -0
  54. package/lib/utils/planDetectionUtils.js.map +1 -0
  55. package/lib/utils/priceUtils.d.ts +23 -0
  56. package/lib/utils/priceUtils.d.ts.map +1 -0
  57. package/lib/utils/priceUtils.js +29 -0
  58. package/lib/utils/priceUtils.js.map +1 -0
  59. package/lib/utils/subscriptionConstants.d.ts +62 -0
  60. package/lib/utils/subscriptionConstants.d.ts.map +1 -0
  61. package/lib/utils/subscriptionConstants.js +61 -0
  62. package/lib/utils/subscriptionConstants.js.map +1 -0
  63. package/package.json +13 -3
  64. package/src/application/ports/ISubscriptionRepository.ts +10 -0
  65. package/src/application/ports/ISubscriptionService.ts +10 -0
  66. package/src/domain/entities/SubscriptionStatus.test.ts +106 -0
  67. package/src/domain/entities/SubscriptionStatus.ts +10 -0
  68. package/src/domain/errors/SubscriptionError.ts +10 -0
  69. package/src/domain/value-objects/SubscriptionConfig.ts +0 -0
  70. package/src/index.ts +9 -2
  71. package/src/infrastructure/services/ActivationHandler.ts +108 -0
  72. package/src/infrastructure/services/SubscriptionService.ts +58 -177
  73. package/src/presentation/hooks/useSubscription.ts +22 -2
  74. package/src/utils/dateUtils.test.ts +116 -0
  75. package/src/utils/dateUtils.ts +12 -76
  76. package/src/utils/dateValidationUtils.test.ts +142 -0
  77. package/src/utils/dateValidationUtils.ts +53 -0
  78. package/src/utils/periodUtils.ts +0 -0
  79. package/src/utils/planDetectionUtils.test.ts +47 -0
  80. package/src/utils/planDetectionUtils.ts +40 -0
  81. package/src/utils/priceUtils.test.ts +35 -0
  82. package/src/utils/priceUtils.ts +0 -0
  83. 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.6",
3
+ "version": "1.1.0",
4
4
  "description": "Subscription management system for React Native apps - Database-first approach with secure validation",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
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"
@@ -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
+
@@ -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
+ });
@@ -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
+
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
+ }