@umituz/react-native-subscription 2.44.0 → 2.45.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/package.json +1 -1
- package/src/domains/paywall/components/PaywallScreen.renderItem.tsx +115 -0
- package/src/domains/paywall/components/PaywallScreen.tsx +41 -119
- package/src/domains/paywall/hooks/usePaywallActions.ts +65 -61
- package/src/domains/paywall/hooks/usePaywallActions.utils.ts +39 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +84 -296
- package/src/domains/subscription/application/sync/CreditDocumentOperations.ts +64 -0
- package/src/domains/subscription/application/sync/PurchaseSyncHandler.ts +83 -0
- package/src/domains/subscription/application/sync/RenewalSyncHandler.ts +69 -0
- package/src/domains/subscription/application/sync/StatusChangeSyncHandler.ts +57 -0
- package/src/domains/subscription/application/sync/SyncProcessorLogger.ts +120 -0
- package/src/domains/subscription/application/sync/UserIdResolver.ts +31 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +187 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +20 -159
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackScreen.parts.tsx +201 -0
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackScreen.tsx +89 -185
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
2
|
+
* Subscription Sync Processor (Refactored)
|
|
3
|
+
*
|
|
4
|
+
* Facade for subscription sync operations.
|
|
5
|
+
* Delegates to specialized handlers for better maintainability.
|
|
11
6
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
50
|
+
// ─────────────────────────────────────────────────────────────
|
|
51
|
+
// PUBLIC API
|
|
52
|
+
// ─────────────────────────────────────────────────────────────
|
|
27
53
|
|
|
28
54
|
async handlePurchase(event: PurchaseCompletedEvent): Promise<{ success: boolean; error?: string }> {
|
|
29
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
phase: 'purchase',
|
|
69
|
+
|
|
70
|
+
this.logger.emitSyncStatus('purchase', 'success', {
|
|
54
71
|
userId: event.userId,
|
|
55
72
|
productId: event.productId,
|
|
56
73
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
phase: 'renewal',
|
|
108
|
+
|
|
109
|
+
this.logger.emitSyncStatus('renewal', 'success', {
|
|
109
110
|
userId: event.userId,
|
|
110
111
|
productId: event.productId,
|
|
111
112
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|