@umituz/react-native-subscription 2.44.1 → 2.45.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.
@@ -1,366 +1,154 @@
1
- import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
2
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
3
- import type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "../core/SubscriptionEvents";
4
- import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
5
- import { extractRevenueCatData } from "./SubscriptionSyncUtils";
6
- import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
7
-
8
1
  /**
9
- * Central processor for all subscription sync operations.
10
- * Handles purchases, renewals, and status changes with credit allocation.
2
+ * Subscription Sync Processor (Refactored)
3
+ *
4
+ * Facade for subscription sync operations.
5
+ * Delegates to specialized handlers for better maintainability.
11
6
  *
12
- * Responsibilities:
13
- * - Purchase: allocate initial credits via atomic Firestore transaction
14
- * - Renewal: allocate renewal credits
15
- * - Status change: sync metadata (no credit allocation) or mark expired
16
- * - Recovery: create missing credits document for premium users
7
+ * Architecture:
8
+ * - SyncProcessorLogger: Centralized logging
9
+ * - UserIdResolver: User ID resolution
10
+ * - PurchaseSyncHandler: Purchase processing
11
+ * - RenewalSyncHandler: Renewal processing
12
+ * - StatusChangeSyncHandler: Status change processing
13
+ * - CreditDocumentOperations: Credit doc operations
17
14
  */
15
+
16
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
17
+ import type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "../core/SubscriptionEvents";
18
+ import { SyncProcessorLogger } from "./sync/SyncProcessorLogger";
19
+ import { UserIdResolver } from "./sync/UserIdResolver";
20
+ import { PurchaseSyncHandler } from "./sync/PurchaseSyncHandler";
21
+ import { RenewalSyncHandler } from "./sync/RenewalSyncHandler";
22
+ import { StatusChangeSyncHandler } from "./sync/StatusChangeSyncHandler";
23
+ import { CreditDocumentOperations } from "./sync/CreditDocumentOperations";
24
+
18
25
  export class SubscriptionSyncProcessor {
19
- private purchaseInProgress = false;
26
+ private logger: SyncProcessorLogger;
27
+ private userIdResolver: UserIdResolver;
28
+ private purchaseHandler: PurchaseSyncHandler;
29
+ private renewalHandler: RenewalSyncHandler;
30
+ private statusChangeHandler: StatusChangeSyncHandler;
31
+ private creditOps: CreditDocumentOperations;
20
32
 
21
33
  constructor(
22
- private entitlementId: string,
23
- private getAnonymousUserId: () => Promise<string>
24
- ) {}
34
+ entitlementId: string,
35
+ getAnonymousUserId: () => Promise<string>
36
+ ) {
37
+ // Initialize dependencies
38
+ this.logger = new SyncProcessorLogger();
39
+ this.userIdResolver = new UserIdResolver(getAnonymousUserId);
40
+ this.creditOps = new CreditDocumentOperations();
41
+ this.purchaseHandler = new PurchaseSyncHandler(entitlementId, this.userIdResolver);
42
+ this.renewalHandler = new RenewalSyncHandler(entitlementId, this.userIdResolver);
43
+ this.statusChangeHandler = new StatusChangeSyncHandler(
44
+ this.userIdResolver,
45
+ this.creditOps,
46
+ this.purchaseHandler
47
+ );
48
+ }
25
49
 
26
- // ─── Public API (replaces SubscriptionSyncService) ────────────────
50
+ // ─────────────────────────────────────────────────────────────
51
+ // PUBLIC API
52
+ // ─────────────────────────────────────────────────────────────
27
53
 
28
54
  async handlePurchase(event: PurchaseCompletedEvent): Promise<{ success: boolean; error?: string }> {
29
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
30
- status: 'syncing',
31
- phase: 'purchase',
55
+ this.logger.emitSyncStatus('purchase', 'syncing', {
32
56
  userId: event.userId,
33
57
  productId: event.productId,
34
58
  });
35
59
 
36
- if (typeof __DEV__ !== "undefined" && __DEV__) {
37
- console.log('[SubscriptionSyncProcessor] 🔵 PURCHASE START', {
38
- userId: event.userId,
39
- productId: event.productId,
40
- source: event.source,
41
- packageType: event.packageType,
42
- timestamp: new Date().toISOString(),
43
- });
44
- }
60
+ this.logger.logPurchaseStart(event);
61
+
45
62
  try {
46
- await this.processPurchase(event);
63
+ await this.purchaseHandler.processPurchase(event);
64
+
47
65
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, {
48
66
  userId: event.userId,
49
67
  productId: event.productId,
50
68
  });
51
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
52
- status: 'success',
53
- phase: 'purchase',
69
+
70
+ this.logger.emitSyncStatus('purchase', 'success', {
54
71
  userId: event.userId,
55
72
  productId: event.productId,
56
73
  });
57
- if (typeof __DEV__ !== "undefined" && __DEV__) {
58
- console.log('[SubscriptionSyncProcessor] 🟢 PURCHASE SUCCESS', {
59
- userId: event.userId,
60
- productId: event.productId,
61
- timestamp: new Date().toISOString(),
62
- });
63
- }
74
+
75
+ this.logger.logPurchaseSuccess(event.userId, event.productId);
76
+
64
77
  return { success: true };
65
78
  } catch (error) {
66
79
  const errorMsg = error instanceof Error ? error.message : String(error);
67
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
68
- status: 'error',
69
- phase: 'purchase',
70
- userId: event.userId,
71
- productId: event.productId,
72
- error: errorMsg,
73
- });
74
- console.error('[SubscriptionSyncProcessor] 🔴 PURCHASE FAILED', {
80
+
81
+ this.logger.emitSyncStatus('purchase', 'error', {
75
82
  userId: event.userId,
76
83
  productId: event.productId,
77
84
  error: errorMsg,
78
- timestamp: new Date().toISOString(),
79
85
  });
86
+
87
+ this.logger.logPurchaseError(event.userId, event.productId, errorMsg);
88
+
80
89
  return { success: false, error: errorMsg };
81
90
  }
82
91
  }
83
92
 
84
93
  async handleRenewal(event: RenewalDetectedEvent): Promise<{ success: boolean; error?: string }> {
85
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
86
- status: 'syncing',
87
- phase: 'renewal',
94
+ this.logger.emitSyncStatus('renewal', 'syncing', {
88
95
  userId: event.userId,
89
96
  productId: event.productId,
90
97
  });
91
98
 
92
- if (typeof __DEV__ !== "undefined" && __DEV__) {
93
- console.log('[SubscriptionSyncProcessor] 🔵 RENEWAL START', {
94
- userId: event.userId,
95
- productId: event.productId,
96
- newExpirationDate: event.newExpirationDate,
97
- timestamp: new Date().toISOString(),
98
- });
99
- }
99
+ this.logger.logRenewalStart(event);
100
+
100
101
  try {
101
- await this.processRenewal(event);
102
+ await this.renewalHandler.processRenewal(event);
103
+
102
104
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, {
103
105
  userId: event.userId,
104
106
  productId: event.productId,
105
107
  });
106
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
107
- status: 'success',
108
- phase: 'renewal',
108
+
109
+ this.logger.emitSyncStatus('renewal', 'success', {
109
110
  userId: event.userId,
110
111
  productId: event.productId,
111
112
  });
112
- if (typeof __DEV__ !== "undefined" && __DEV__) {
113
- console.log('[SubscriptionSyncProcessor] 🟢 RENEWAL SUCCESS', {
114
- userId: event.userId,
115
- productId: event.productId,
116
- timestamp: new Date().toISOString(),
117
- });
118
- }
113
+
114
+ this.logger.logRenewalSuccess(event.userId, event.productId);
115
+
119
116
  return { success: true };
120
117
  } catch (error) {
121
118
  const errorMsg = error instanceof Error ? error.message : String(error);
122
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
123
- status: 'error',
124
- phase: 'renewal',
125
- userId: event.userId,
126
- productId: event.productId,
127
- error: errorMsg,
128
- });
129
- console.error('[SubscriptionSyncProcessor] 🔴 RENEWAL FAILED', {
119
+
120
+ this.logger.emitSyncStatus('renewal', 'error', {
130
121
  userId: event.userId,
131
122
  productId: event.productId,
132
123
  error: errorMsg,
133
- timestamp: new Date().toISOString(),
134
124
  });
125
+
126
+ this.logger.logRenewalError(event.userId, event.productId, errorMsg);
127
+
135
128
  return { success: false, error: errorMsg };
136
129
  }
137
130
  }
138
131
 
139
132
  async handlePremiumStatusChanged(event: PremiumStatusChangedEvent): Promise<{ success: boolean; error?: string }> {
140
- if (typeof __DEV__ !== "undefined" && __DEV__) {
141
- console.log('[SubscriptionSyncProcessor] 🔵 STATUS CHANGE START', {
142
- userId: event.userId,
143
- isPremium: event.isPremium,
144
- productId: event.productId,
145
- willRenew: event.willRenew,
146
- expirationDate: event.expirationDate,
147
- timestamp: new Date().toISOString(),
148
- });
149
- }
133
+ this.logger.logStatusChangeStart(event);
134
+
150
135
  try {
151
- await this.processStatusChange(event);
136
+ await this.statusChangeHandler.processStatusChange(event);
137
+
152
138
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, {
153
139
  userId: event.userId,
154
140
  isPremium: event.isPremium,
155
141
  });
156
- if (typeof __DEV__ !== "undefined" && __DEV__) {
157
- console.log('[SubscriptionSyncProcessor] 🟢 STATUS CHANGE SUCCESS', {
158
- userId: event.userId,
159
- isPremium: event.isPremium,
160
- productId: event.productId,
161
- timestamp: new Date().toISOString(),
162
- });
163
- }
142
+
143
+ this.logger.logStatusChangeSuccess(event.userId, event.isPremium, event.productId);
144
+
164
145
  return { success: true };
165
146
  } catch (error) {
166
147
  const errorMsg = error instanceof Error ? error.message : String(error);
167
- console.error('[SubscriptionSyncProcessor] 🔴 STATUS CHANGE FAILED', {
168
- userId: event.userId,
169
- isPremium: event.isPremium,
170
- productId: event.productId,
171
- error: errorMsg,
172
- timestamp: new Date().toISOString(),
173
- });
174
- // We don't emit sync status change here for passive status changes to avoid UI noise
175
- return { success: false, error: errorMsg };
176
- }
177
- }
178
-
179
- // ─── Internal Processing ──────────────────────────────────────────
180
-
181
- private async getCreditsUserId(revenueCatUserId: string | null | undefined): Promise<string> {
182
- const trimmed = revenueCatUserId?.trim();
183
- if (trimmed && trimmed.length > 0 && trimmed !== 'undefined' && trimmed !== 'null') {
184
- return trimmed;
185
- }
186
-
187
- console.warn("[SubscriptionSyncProcessor] revenueCatUserId is empty/null, falling back to anonymousUserId");
188
- const anonymousId = await this.getAnonymousUserId();
189
- const trimmedAnonymous = anonymousId?.trim();
190
- if (!trimmedAnonymous || trimmedAnonymous.length === 0 || trimmedAnonymous === 'undefined' || trimmedAnonymous === 'null') {
191
- throw new Error("[SubscriptionSyncProcessor] Cannot resolve credits userId: both revenueCatUserId and anonymousUserId are empty");
192
- }
193
- return trimmedAnonymous;
194
- }
195
-
196
- private async processPurchase(event: PurchaseCompletedEvent): Promise<void> {
197
- this.purchaseInProgress = true;
198
- if (typeof __DEV__ !== "undefined" && __DEV__) {
199
- console.log('[SubscriptionSyncProcessor] 🔵 processPurchase: Starting credit initialization', {
200
- productId: event.productId,
201
- source: event.source,
202
- packageType: event.packageType,
203
- activeEntitlements: Object.keys(event.customerInfo.entitlements.active),
204
- });
205
- }
206
- try {
207
- const revenueCatData = extractRevenueCatData(event.customerInfo, this.entitlementId);
208
- revenueCatData.packageType = event.packageType ?? null;
209
- // Use the event.userId instead of polling the SDK to avoid race conditions during rapid user switching
210
- revenueCatData.revenueCatUserId = event.userId;
211
- const purchaseId = generatePurchaseId(revenueCatData.storeTransactionId, event.productId);
212
-
213
- const creditsUserId = await this.getCreditsUserId(event.userId);
214
148
 
215
- if (typeof __DEV__ !== "undefined" && __DEV__) {
216
- console.log('[SubscriptionSyncProcessor] 🔵 processPurchase: Calling initializeCredits', {
217
- creditsUserId,
218
- purchaseId,
219
- productId: event.productId,
220
- revenueCatUserId: revenueCatData.revenueCatUserId,
221
- });
222
- }
149
+ this.logger.logStatusChangeError(event.userId, event.isPremium, event.productId, errorMsg);
223
150
 
224
- const result = await getCreditsRepository().initializeCredits(
225
- creditsUserId,
226
- purchaseId,
227
- event.productId,
228
- event.source ?? PURCHASE_SOURCE.SETTINGS,
229
- revenueCatData,
230
- PURCHASE_TYPE.INITIAL
231
- );
232
-
233
- if (!result.success) {
234
- throw new Error(`[SubscriptionSyncProcessor] Credit initialization failed for purchase: ${result.error?.message ?? 'unknown'}`);
235
- }
236
-
237
- if (typeof __DEV__ !== "undefined" && __DEV__) {
238
- console.log('[SubscriptionSyncProcessor] 🟢 processPurchase: Credits initialized successfully', {
239
- creditsUserId,
240
- purchaseId,
241
- credits: result.data?.credits,
242
- });
243
- }
244
- } finally {
245
- this.purchaseInProgress = false;
246
- }
247
- }
248
-
249
- private async processRenewal(event: RenewalDetectedEvent): Promise<void> {
250
- this.purchaseInProgress = true;
251
- try {
252
- const revenueCatData = extractRevenueCatData(event.customerInfo, this.entitlementId);
253
- revenueCatData.expirationDate = event.newExpirationDate ?? revenueCatData.expirationDate;
254
- // Use the event.userId instead of polling the SDK to avoid race conditions during rapid user switching
255
- revenueCatData.revenueCatUserId = event.userId;
256
- const purchaseId = generateRenewalId(revenueCatData.storeTransactionId, event.productId, event.newExpirationDate);
257
-
258
- const creditsUserId = await this.getCreditsUserId(event.userId);
259
-
260
- const result = await getCreditsRepository().initializeCredits(
261
- creditsUserId,
262
- purchaseId,
263
- event.productId,
264
- PURCHASE_SOURCE.RENEWAL,
265
- revenueCatData,
266
- PURCHASE_TYPE.RENEWAL
267
- );
268
-
269
- if (!result.success) {
270
- throw new Error(`[SubscriptionSyncProcessor] Credit initialization failed for renewal: ${result.error?.message ?? 'unknown'}`);
271
- }
272
-
273
- } finally {
274
- this.purchaseInProgress = false;
275
- }
276
- }
277
-
278
- private async processStatusChange(event: PremiumStatusChangedEvent): Promise<void> {
279
- if (this.purchaseInProgress) {
280
- if (typeof __DEV__ !== "undefined" && __DEV__) {
281
- console.log("[SubscriptionSyncProcessor] Purchase in progress - running recovery only");
282
- }
283
- if (event.isPremium && event.productId) {
284
- const creditsUserId = await this.getCreditsUserId(event.userId);
285
- await this.syncPremiumStatus(creditsUserId, event);
286
- }
287
- return;
288
- }
289
-
290
- const creditsUserId = await this.getCreditsUserId(event.userId);
291
-
292
- if (!event.isPremium && event.productId) {
293
- await this.expireSubscription(creditsUserId);
294
- return;
295
- }
296
-
297
- if (!event.isPremium && !event.productId) {
298
- const hasDoc = await getCreditsRepository().creditsDocumentExists(creditsUserId);
299
- if (hasDoc) {
300
- await this.expireSubscription(creditsUserId);
301
- }
302
- return;
303
- }
304
-
305
- if (!event.productId) {
306
- return;
307
- }
308
-
309
- await this.syncPremiumStatus(creditsUserId, event);
310
- }
311
-
312
- // ─── Credit Document Operations ───
313
-
314
- private async expireSubscription(userId: string): Promise<void> {
315
- await getCreditsRepository().syncExpiredStatus(userId);
316
- }
317
-
318
- private async syncPremiumStatus(userId: string, event: PremiumStatusChangedEvent): Promise<void> {
319
- const repo = getCreditsRepository();
320
-
321
- if (__DEV__) {
322
- console.log('[SubscriptionSyncProcessor] 🔵 syncPremiumStatus: Starting', {
323
- userId,
324
- isPremium: event.isPremium,
325
- productId: event.productId,
326
- willRenew: event.willRenew,
327
- });
328
- }
329
-
330
- if (event.isPremium) {
331
- const created = await repo.ensurePremiumCreditsExist(
332
- userId,
333
- event.productId!,
334
- event.willRenew ?? false,
335
- event.expirationDate ?? null,
336
- event.periodType ?? null,
337
- );
338
- if (__DEV__ && created) {
339
- console.log('[SubscriptionSyncProcessor] 🟢 Recovery: created missing credits document for premium user', {
340
- userId,
341
- productId: event.productId,
342
- });
343
- }
344
- }
345
-
346
- await repo.syncPremiumMetadata(userId, {
347
- isPremium: event.isPremium,
348
- willRenew: event.willRenew ?? false,
349
- expirationDate: event.expirationDate ?? null,
350
- productId: event.productId!,
351
- periodType: event.periodType ?? null,
352
- unsubscribeDetectedAt: event.unsubscribeDetectedAt ?? null,
353
- billingIssueDetectedAt: event.billingIssueDetectedAt ?? null,
354
- store: event.store ?? null,
355
- ownershipType: event.ownershipType ?? null,
356
- });
357
-
358
- if (__DEV__) {
359
- console.log('[SubscriptionSyncProcessor] 🟢 syncPremiumStatus: Completed', {
360
- userId,
361
- isPremium: event.isPremium,
362
- productId: event.productId,
363
- });
151
+ return { success: false, error: errorMsg };
364
152
  }
365
153
  }
366
154
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Credit Document Operations
3
+ * Handles Firestore credit document operations for subscription sync
4
+ */
5
+
6
+ import { getCreditsRepository } from "../../../credits/infrastructure/CreditsRepositoryManager";
7
+ import type { PremiumStatusChangedEvent } from "../../core/SubscriptionEvents";
8
+
9
+ export class CreditDocumentOperations {
10
+ async expireSubscription(userId: string): Promise<void> {
11
+ await getCreditsRepository().syncExpiredStatus(userId);
12
+ }
13
+
14
+ async syncPremiumStatus(userId: string, event: PremiumStatusChangedEvent): Promise<void> {
15
+ const repo = getCreditsRepository();
16
+
17
+ if (__DEV__) {
18
+ console.log('[CreditDocumentOperations] 🔵 syncPremiumStatus: Starting', {
19
+ userId,
20
+ isPremium: event.isPremium,
21
+ productId: event.productId,
22
+ willRenew: event.willRenew,
23
+ });
24
+ }
25
+
26
+ // Ensure premium user has a credits document (recovery)
27
+ if (event.isPremium) {
28
+ const created = await repo.ensurePremiumCreditsExist(
29
+ userId,
30
+ event.productId!,
31
+ event.willRenew ?? false,
32
+ event.expirationDate ?? null,
33
+ event.periodType ?? null,
34
+ );
35
+ if (__DEV__ && created) {
36
+ console.log('[CreditDocumentOperations] 🟢 Recovery: created missing credits document for premium user', {
37
+ userId,
38
+ productId: event.productId,
39
+ });
40
+ }
41
+ }
42
+
43
+ // Sync premium metadata
44
+ await repo.syncPremiumMetadata(userId, {
45
+ isPremium: event.isPremium,
46
+ willRenew: event.willRenew ?? false,
47
+ expirationDate: event.expirationDate ?? null,
48
+ productId: event.productId!,
49
+ periodType: event.periodType ?? null,
50
+ unsubscribeDetectedAt: event.unsubscribeDetectedAt ?? null,
51
+ billingIssueDetectedAt: event.billingIssueDetectedAt ?? null,
52
+ store: event.store ?? null,
53
+ ownershipType: event.ownershipType ?? null,
54
+ });
55
+
56
+ if (__DEV__) {
57
+ console.log('[CreditDocumentOperations] 🟢 syncPremiumStatus: Completed', {
58
+ userId,
59
+ isPremium: event.isPremium,
60
+ productId: event.productId,
61
+ });
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Purchase Sync Handler
3
+ * Handles initial purchase credit allocation
4
+ */
5
+
6
+ import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../../core/SubscriptionConstants";
7
+ import { getCreditsRepository } from "../../../credits/infrastructure/CreditsRepositoryManager";
8
+ import { extractRevenueCatData } from "../SubscriptionSyncUtils";
9
+ import { generatePurchaseId } from "../syncIdGenerators";
10
+ import type { PurchaseCompletedEvent } from "../../core/SubscriptionEvents";
11
+ import { UserIdResolver } from "./UserIdResolver";
12
+
13
+ export class PurchaseSyncHandler {
14
+ private purchaseInProgress = false;
15
+
16
+ constructor(
17
+ private entitlementId: string,
18
+ private userIdResolver: UserIdResolver
19
+ ) {}
20
+
21
+ isProcessing(): boolean {
22
+ return this.purchaseInProgress;
23
+ }
24
+
25
+ async processPurchase(event: PurchaseCompletedEvent): Promise<void> {
26
+ this.purchaseInProgress = true;
27
+
28
+ if (__DEV__) {
29
+ console.log('[PurchaseSyncHandler] 🔵 Starting credit initialization', {
30
+ productId: event.productId,
31
+ source: event.source,
32
+ packageType: event.packageType,
33
+ activeEntitlements: Object.keys(event.customerInfo.entitlements.active),
34
+ });
35
+ }
36
+
37
+ try {
38
+ // Extract revenue cat data
39
+ const revenueCatData = extractRevenueCatData(event.customerInfo, this.entitlementId);
40
+ revenueCatData.packageType = event.packageType ?? null;
41
+ revenueCatData.revenueCatUserId = event.userId;
42
+
43
+ // Generate purchase ID
44
+ const purchaseId = generatePurchaseId(revenueCatData.storeTransactionId, event.productId);
45
+
46
+ // Resolve user ID
47
+ const creditsUserId = await this.userIdResolver.resolveCreditsUserId(event.userId);
48
+
49
+ if (__DEV__) {
50
+ console.log('[PurchaseSyncHandler] 🔵 Calling initializeCredits', {
51
+ creditsUserId,
52
+ purchaseId,
53
+ productId: event.productId,
54
+ revenueCatUserId: revenueCatData.revenueCatUserId,
55
+ });
56
+ }
57
+
58
+ // Initialize credits
59
+ const result = await getCreditsRepository().initializeCredits(
60
+ creditsUserId,
61
+ purchaseId,
62
+ event.productId,
63
+ event.source ?? PURCHASE_SOURCE.SETTINGS,
64
+ revenueCatData,
65
+ PURCHASE_TYPE.INITIAL
66
+ );
67
+
68
+ if (!result.success) {
69
+ throw new Error(`[PurchaseSyncHandler] Credit initialization failed: ${result.error?.message ?? 'unknown'}`);
70
+ }
71
+
72
+ if (__DEV__) {
73
+ console.log('[PurchaseSyncHandler] 🟢 Credits initialized successfully', {
74
+ creditsUserId,
75
+ purchaseId,
76
+ credits: result.data?.credits,
77
+ });
78
+ }
79
+ } finally {
80
+ this.purchaseInProgress = false;
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Renewal Sync Handler
3
+ * Handles subscription renewal credit allocation
4
+ */
5
+
6
+ import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../../core/SubscriptionConstants";
7
+ import { getCreditsRepository } from "../../../credits/infrastructure/CreditsRepositoryManager";
8
+ import { extractRevenueCatData } from "../SubscriptionSyncUtils";
9
+ import { generateRenewalId } from "../syncIdGenerators";
10
+ import type { RenewalDetectedEvent } from "../../core/SubscriptionEvents";
11
+ import { UserIdResolver } from "./UserIdResolver";
12
+
13
+ export class RenewalSyncHandler {
14
+ private renewalInProgress = false;
15
+
16
+ constructor(
17
+ private entitlementId: string,
18
+ private userIdResolver: UserIdResolver
19
+ ) {}
20
+
21
+ isProcessing(): boolean {
22
+ return this.renewalInProgress;
23
+ }
24
+
25
+ async processRenewal(event: RenewalDetectedEvent): Promise<void> {
26
+ this.renewalInProgress = true;
27
+
28
+ try {
29
+ // Extract revenue cat data
30
+ const revenueCatData = extractRevenueCatData(event.customerInfo, this.entitlementId);
31
+ revenueCatData.expirationDate = event.newExpirationDate ?? revenueCatData.expirationDate;
32
+ revenueCatData.revenueCatUserId = event.userId;
33
+
34
+ // Generate renewal ID
35
+ const purchaseId = generateRenewalId(
36
+ revenueCatData.storeTransactionId,
37
+ event.productId,
38
+ event.newExpirationDate
39
+ );
40
+
41
+ // Resolve user ID
42
+ const creditsUserId = await this.userIdResolver.resolveCreditsUserId(event.userId);
43
+
44
+ // Initialize credits for renewal
45
+ const result = await getCreditsRepository().initializeCredits(
46
+ creditsUserId,
47
+ purchaseId,
48
+ event.productId,
49
+ PURCHASE_SOURCE.RENEWAL,
50
+ revenueCatData,
51
+ PURCHASE_TYPE.RENEWAL
52
+ );
53
+
54
+ if (!result.success) {
55
+ throw new Error(`[RenewalSyncHandler] Credit initialization failed: ${result.error?.message ?? 'unknown'}`);
56
+ }
57
+
58
+ if (__DEV__) {
59
+ console.log('[RenewalSyncHandler] 🟢 Renewal credits allocated successfully', {
60
+ creditsUserId,
61
+ purchaseId,
62
+ productId: event.productId,
63
+ });
64
+ }
65
+ } finally {
66
+ this.renewalInProgress = false;
67
+ }
68
+ }
69
+ }