@umituz/react-native-subscription 2.39.13 → 2.40.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.
- package/package.json +1 -1
- package/src/domains/credits/application/CreditsInitializer.ts +63 -20
- package/src/domains/credits/core/CreditsConstants.ts +5 -4
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +17 -20
- package/src/domains/paywall/hooks/usePaywallActions.ts +36 -0
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +97 -4
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +16 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.40.1",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -8,7 +8,7 @@ import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
|
|
|
8
8
|
import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
|
|
9
9
|
import { calculateCreditLimit } from "./CreditLimitCalculator";
|
|
10
10
|
import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
|
|
11
|
-
import { PURCHASE_ID_PREFIXES,
|
|
11
|
+
import { PURCHASE_ID_PREFIXES, TRANSACTION_SUBCOLLECTION } from "../core/CreditsConstants";
|
|
12
12
|
|
|
13
13
|
export async function initializeCreditsTransaction(
|
|
14
14
|
_db: Firestore,
|
|
@@ -19,6 +19,20 @@ export async function initializeCreditsTransaction(
|
|
|
19
19
|
userId: string
|
|
20
20
|
): Promise<InitializationResult> {
|
|
21
21
|
|
|
22
|
+
if (__DEV__) {
|
|
23
|
+
console.log('[CreditsInitializer] 🔵 initializeCreditsTransaction: START', {
|
|
24
|
+
userId,
|
|
25
|
+
purchaseId,
|
|
26
|
+
productId: metadata.productId,
|
|
27
|
+
source: metadata.source,
|
|
28
|
+
type: metadata.type,
|
|
29
|
+
isPremium: metadata.isPremium,
|
|
30
|
+
willRenew: metadata.willRenew,
|
|
31
|
+
storeTransactionId: metadata.storeTransactionId,
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
if (!purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) && !purchaseId.startsWith(PURCHASE_ID_PREFIXES.RENEWAL)) {
|
|
23
37
|
throw new Error(`[CreditsInitializer] Only purchase and renewal operations can allocate credits. Received: ${purchaseId}`);
|
|
24
38
|
}
|
|
@@ -32,6 +46,14 @@ export async function initializeCreditsTransaction(
|
|
|
32
46
|
const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
|
|
33
47
|
|
|
34
48
|
if (existingData.processedPurchases.includes(purchaseId)) {
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.log('[CreditsInitializer] 🟡 Transaction already processed, skipping', {
|
|
51
|
+
userId,
|
|
52
|
+
purchaseId,
|
|
53
|
+
existingCredits: existingData.credits,
|
|
54
|
+
processedPurchasesCount: existingData.processedPurchases.length,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
35
57
|
return {
|
|
36
58
|
credits: existingData.credits,
|
|
37
59
|
alreadyProcessed: true,
|
|
@@ -39,26 +61,37 @@ export async function initializeCreditsTransaction(
|
|
|
39
61
|
};
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
//
|
|
43
|
-
// from allocating credits
|
|
64
|
+
// User-specific transaction deduplication: prevent the same Apple/Google transaction
|
|
65
|
+
// from allocating credits multiple times for the same user.
|
|
66
|
+
// Path: users/{userId}/credits/processedTransactions/{transactionId}
|
|
44
67
|
if (metadata.storeTransactionId) {
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
credits: existingData.credits,
|
|
55
|
-
alreadyProcessed: true,
|
|
56
|
-
finalData: existingData
|
|
57
|
-
};
|
|
68
|
+
const transactionRef = doc(_db, creditsRef.path, TRANSACTION_SUBCOLLECTION, metadata.storeTransactionId);
|
|
69
|
+
const transactionDoc = await transaction.get(transactionRef);
|
|
70
|
+
if (transactionDoc.exists()) {
|
|
71
|
+
if (__DEV__) {
|
|
72
|
+
console.log('[CreditsInitializer] 🟡 Store transaction already processed, skipping', {
|
|
73
|
+
userId,
|
|
74
|
+
storeTransactionId: metadata.storeTransactionId,
|
|
75
|
+
existingCredits: existingData.credits,
|
|
76
|
+
});
|
|
58
77
|
}
|
|
78
|
+
return {
|
|
79
|
+
credits: existingData.credits,
|
|
80
|
+
alreadyProcessed: true,
|
|
81
|
+
finalData: existingData
|
|
82
|
+
};
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
|
|
86
|
+
if (__DEV__) {
|
|
87
|
+
console.log('[CreditsInitializer] 🔵 Processing credit allocation', {
|
|
88
|
+
userId,
|
|
89
|
+
purchaseId,
|
|
90
|
+
existingCredits: existingData.credits,
|
|
91
|
+
productId: metadata.productId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
62
95
|
const creditLimit = calculateCreditLimit(metadata.productId, config);
|
|
63
96
|
const { purchaseHistory } = generatePurchaseMetadata({
|
|
64
97
|
productId: metadata.productId,
|
|
@@ -88,17 +121,27 @@ export async function initializeCreditsTransaction(
|
|
|
88
121
|
|
|
89
122
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
90
123
|
|
|
91
|
-
// Register transaction
|
|
124
|
+
// Register transaction in user-specific subcollection to prevent duplicate processing.
|
|
92
125
|
if (metadata.storeTransactionId) {
|
|
93
|
-
const
|
|
94
|
-
transaction.set(
|
|
95
|
-
ownerUserId: userId,
|
|
126
|
+
const transactionRef = doc(_db, creditsRef.path, TRANSACTION_SUBCOLLECTION, metadata.storeTransactionId);
|
|
127
|
+
transaction.set(transactionRef, {
|
|
96
128
|
purchaseId,
|
|
97
129
|
productId: metadata.productId,
|
|
98
130
|
createdAt: serverTimestamp(),
|
|
99
131
|
});
|
|
100
132
|
}
|
|
101
133
|
|
|
134
|
+
if (__DEV__) {
|
|
135
|
+
console.log('[CreditsInitializer] 🟢 Credit allocation successful', {
|
|
136
|
+
userId,
|
|
137
|
+
purchaseId,
|
|
138
|
+
previousCredits: existingData.credits,
|
|
139
|
+
newCredits,
|
|
140
|
+
creditLimit,
|
|
141
|
+
productId: metadata.productId,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
102
145
|
return {
|
|
103
146
|
credits: newCredits,
|
|
104
147
|
alreadyProcessed: false,
|
|
@@ -16,8 +16,9 @@ export const PROCESSED_PURCHASES_WINDOW = 50;
|
|
|
16
16
|
export const MAX_SINGLE_DEDUCTION = 10000;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* User-specific Firestore sub-collection for transaction deduplication.
|
|
20
|
+
* Changed from global root-level collection to user-scoped collection
|
|
21
|
+
* to match vivoim_app pattern and avoid Firestore permission issues.
|
|
22
|
+
* Path: users/{userId}/credits/processedTransactions/{transactionId}
|
|
22
23
|
*/
|
|
23
|
-
export const
|
|
24
|
+
export const TRANSACTION_SUBCOLLECTION = 'processedTransactions';
|
|
@@ -7,7 +7,7 @@ import type { SubscriptionMetadata } from "../../../subscription/core/types/Subs
|
|
|
7
7
|
import { toTimestamp } from "../../../../shared/utils/dateConverter";
|
|
8
8
|
import { isPast } from "../../../../utils/dateUtils";
|
|
9
9
|
import { getAppVersion, validatePlatform } from "../../../../utils/appUtils";
|
|
10
|
-
import {
|
|
10
|
+
import { TRANSACTION_SUBCOLLECTION } from "../../core/CreditsConstants";
|
|
11
11
|
|
|
12
12
|
// Fix: was getDoc+setDoc (non-atomic) — now uses runTransaction so concurrent
|
|
13
13
|
// initializeCreditsTransaction and deductCreditsOperation no longer see stale
|
|
@@ -67,17 +67,13 @@ export async function syncPremiumMetadata(
|
|
|
67
67
|
* This handles edge cases like test store purchases, reinstalls, or failed initializations.
|
|
68
68
|
* Returns true if a new document was created, false if one already existed.
|
|
69
69
|
*
|
|
70
|
-
* Cross-user guard: if storeTransactionId is provided and already registered
|
|
71
|
-
* to a different user in the global processedTransactions collection, the recovery
|
|
72
|
-
* document is NOT created (the subscription belongs to another UID).
|
|
73
|
-
*
|
|
74
70
|
* NOTE: This uses non-atomic check-then-act (getDoc + setDoc). In theory, two concurrent
|
|
75
71
|
* calls could both see no document and create duplicates. However, this is extremely rare
|
|
76
72
|
* in practice because: (1) createRecoveryCreditsDocument is called after a successful
|
|
77
|
-
* purchase which is already serialized, (2) the
|
|
78
|
-
* duplicates
|
|
73
|
+
* purchase which is already serialized, (2) the user-specific transaction check (below) prevents
|
|
74
|
+
* duplicates, (3) even if two recovery docs are created, the credits document
|
|
79
75
|
* logic is idempotent (same purchaseId processed twice is no-op). Making this atomic
|
|
80
|
-
* would require a transaction spanning both the credits doc and
|
|
76
|
+
* would require a transaction spanning both the credits doc and transaction subcollection,
|
|
81
77
|
* which adds complexity without meaningful benefit given the safeguards above.
|
|
82
78
|
*/
|
|
83
79
|
export async function createRecoveryCreditsDocument(
|
|
@@ -94,24 +90,25 @@ export async function createRecoveryCreditsDocument(
|
|
|
94
90
|
const existingDoc = await getDoc(ref);
|
|
95
91
|
if (existingDoc.exists()) return false;
|
|
96
92
|
|
|
97
|
-
//
|
|
98
|
-
// user, skip recovery to prevent
|
|
93
|
+
// User-specific deduplication: if this transaction was already processed
|
|
94
|
+
// for this user, skip recovery to prevent duplicate credit allocation.
|
|
99
95
|
if (db && userId && storeTransactionId) {
|
|
100
96
|
try {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`[CreditsWriter] Recovery skipped: transaction ${storeTransactionId} belongs to user ${globalData.ownerUserId}, not ${userId}`
|
|
97
|
+
const transactionRef = doc(db, ref.path, TRANSACTION_SUBCOLLECTION, storeTransactionId);
|
|
98
|
+
const transactionDoc = await getDoc(transactionRef);
|
|
99
|
+
if (transactionDoc.exists()) {
|
|
100
|
+
if (__DEV__) {
|
|
101
|
+
console.log(
|
|
102
|
+
`[CreditsWriter] Recovery skipped: transaction ${storeTransactionId} already processed for user ${userId}`
|
|
108
103
|
);
|
|
109
|
-
return false;
|
|
110
104
|
}
|
|
105
|
+
return false;
|
|
111
106
|
}
|
|
112
107
|
} catch (error) {
|
|
113
|
-
// Non-fatal: if
|
|
114
|
-
|
|
108
|
+
// Non-fatal: if transaction check fails, still create recovery doc as safety net
|
|
109
|
+
if (__DEV__) {
|
|
110
|
+
console.warn('[CreditsWriter] Transaction check failed during recovery:', error);
|
|
111
|
+
}
|
|
115
112
|
}
|
|
116
113
|
}
|
|
117
114
|
|
|
@@ -74,14 +74,33 @@ export function usePaywallActions({
|
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
if (__DEV__) {
|
|
78
|
+
console.log('[usePaywallActions] Starting purchase', {
|
|
79
|
+
productId: pkg.product.identifier,
|
|
80
|
+
hasOnClose: !!onCloseRef.current,
|
|
81
|
+
hasOnSuccess: !!onPurchaseSuccessRef.current,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
setIsLocalProcessing(true);
|
|
78
86
|
startPurchase(currentSelectedId, "manual");
|
|
79
87
|
|
|
80
88
|
try {
|
|
81
89
|
const success = await onPurchaseRef.current(pkg);
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
console.log('[usePaywallActions] Purchase completed', { success });
|
|
92
|
+
}
|
|
82
93
|
if (success === true) {
|
|
94
|
+
if (__DEV__) {
|
|
95
|
+
console.log('[usePaywallActions] Purchase successful, calling onPurchaseSuccess and onClose');
|
|
96
|
+
}
|
|
83
97
|
onPurchaseSuccessRef.current?.();
|
|
98
|
+
// Always close paywall on successful purchase
|
|
84
99
|
onCloseRef.current?.();
|
|
100
|
+
} else {
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.warn('[usePaywallActions] Purchase returned false, not closing');
|
|
103
|
+
}
|
|
85
104
|
}
|
|
86
105
|
} catch (error) {
|
|
87
106
|
onPurchaseErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -99,12 +118,29 @@ export function usePaywallActions({
|
|
|
99
118
|
|
|
100
119
|
if (isProcessingRef.current) return;
|
|
101
120
|
|
|
121
|
+
if (__DEV__) {
|
|
122
|
+
console.log('[usePaywallActions] Starting restore', {
|
|
123
|
+
hasOnClose: !!onCloseRef.current,
|
|
124
|
+
hasOnSuccess: !!onPurchaseSuccessRef.current,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
setIsLocalProcessing(true);
|
|
103
129
|
try {
|
|
104
130
|
const success = await onRestoreRef.current();
|
|
131
|
+
if (__DEV__) {
|
|
132
|
+
console.log('[usePaywallActions] Restore completed', { success });
|
|
133
|
+
}
|
|
105
134
|
if (success === true) {
|
|
135
|
+
if (__DEV__) {
|
|
136
|
+
console.log('[usePaywallActions] Restore successful, calling onPurchaseSuccess and onClose');
|
|
137
|
+
}
|
|
106
138
|
onPurchaseSuccessRef.current?.();
|
|
107
139
|
onCloseRef.current?.();
|
|
140
|
+
} else {
|
|
141
|
+
if (__DEV__) {
|
|
142
|
+
console.warn('[usePaywallActions] Restore returned false, not closing');
|
|
143
|
+
}
|
|
108
144
|
}
|
|
109
145
|
} catch (error) {
|
|
110
146
|
onPurchaseErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -26,52 +26,104 @@ export class SubscriptionSyncProcessor {
|
|
|
26
26
|
// ─── Public API (replaces SubscriptionSyncService) ────────────────
|
|
27
27
|
|
|
28
28
|
async handlePurchase(event: PurchaseCompletedEvent): Promise<void> {
|
|
29
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
30
|
+
console.log('[SubscriptionSyncProcessor] 🔵 PURCHASE START', {
|
|
31
|
+
userId: event.userId,
|
|
32
|
+
productId: event.productId,
|
|
33
|
+
source: event.source,
|
|
34
|
+
packageType: event.packageType,
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
29
38
|
try {
|
|
30
39
|
await this.processPurchase(event);
|
|
31
40
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, {
|
|
32
41
|
userId: event.userId,
|
|
33
42
|
productId: event.productId,
|
|
34
43
|
});
|
|
44
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
+
console.log('[SubscriptionSyncProcessor] 🟢 PURCHASE SUCCESS', {
|
|
46
|
+
userId: event.userId,
|
|
47
|
+
productId: event.productId,
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
35
51
|
} catch (error) {
|
|
36
|
-
console.error('[SubscriptionSyncProcessor]
|
|
52
|
+
console.error('[SubscriptionSyncProcessor] 🔴 PURCHASE FAILED', {
|
|
37
53
|
userId: event.userId,
|
|
38
54
|
productId: event.productId,
|
|
39
55
|
error: error instanceof Error ? error.message : String(error),
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
40
57
|
});
|
|
41
58
|
throw error;
|
|
42
59
|
}
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
async handleRenewal(event: RenewalDetectedEvent): Promise<void> {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
+
console.log('[SubscriptionSyncProcessor] 🔵 RENEWAL START', {
|
|
65
|
+
userId: event.userId,
|
|
66
|
+
productId: event.productId,
|
|
67
|
+
newExpirationDate: event.newExpirationDate,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
46
71
|
try {
|
|
47
72
|
await this.processRenewal(event);
|
|
48
73
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, {
|
|
49
74
|
userId: event.userId,
|
|
50
75
|
productId: event.productId,
|
|
51
76
|
});
|
|
77
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
+
console.log('[SubscriptionSyncProcessor] 🟢 RENEWAL SUCCESS', {
|
|
79
|
+
userId: event.userId,
|
|
80
|
+
productId: event.productId,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
52
84
|
} catch (error) {
|
|
53
|
-
console.error('[SubscriptionSyncProcessor]
|
|
85
|
+
console.error('[SubscriptionSyncProcessor] 🔴 RENEWAL FAILED', {
|
|
54
86
|
userId: event.userId,
|
|
55
87
|
productId: event.productId,
|
|
56
88
|
error: error instanceof Error ? error.message : String(error),
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
57
90
|
});
|
|
58
91
|
throw error;
|
|
59
92
|
}
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
async handlePremiumStatusChanged(event: PremiumStatusChangedEvent): Promise<void> {
|
|
96
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
+
console.log('[SubscriptionSyncProcessor] 🔵 STATUS CHANGE START', {
|
|
98
|
+
userId: event.userId,
|
|
99
|
+
isPremium: event.isPremium,
|
|
100
|
+
productId: event.productId,
|
|
101
|
+
willRenew: event.willRenew,
|
|
102
|
+
expirationDate: event.expirationDate,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
63
106
|
try {
|
|
64
107
|
await this.processStatusChange(event);
|
|
65
108
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, {
|
|
66
109
|
userId: event.userId,
|
|
67
110
|
isPremium: event.isPremium,
|
|
68
111
|
});
|
|
112
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
113
|
+
console.log('[SubscriptionSyncProcessor] 🟢 STATUS CHANGE SUCCESS', {
|
|
114
|
+
userId: event.userId,
|
|
115
|
+
isPremium: event.isPremium,
|
|
116
|
+
productId: event.productId,
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
69
120
|
} catch (error) {
|
|
70
|
-
console.error('[SubscriptionSyncProcessor]
|
|
121
|
+
console.error('[SubscriptionSyncProcessor] 🔴 STATUS CHANGE FAILED', {
|
|
71
122
|
userId: event.userId,
|
|
72
123
|
isPremium: event.isPremium,
|
|
73
124
|
productId: event.productId,
|
|
74
125
|
error: error instanceof Error ? error.message : String(error),
|
|
126
|
+
timestamp: new Date().toISOString(),
|
|
75
127
|
});
|
|
76
128
|
throw error;
|
|
77
129
|
}
|
|
@@ -96,6 +148,14 @@ export class SubscriptionSyncProcessor {
|
|
|
96
148
|
|
|
97
149
|
private async processPurchase(event: PurchaseCompletedEvent): Promise<void> {
|
|
98
150
|
this.purchaseInProgress = true;
|
|
151
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
152
|
+
console.log('[SubscriptionSyncProcessor] 🔵 processPurchase: Starting credit initialization', {
|
|
153
|
+
productId: event.productId,
|
|
154
|
+
source: event.source,
|
|
155
|
+
packageType: event.packageType,
|
|
156
|
+
activeEntitlements: Object.keys(event.customerInfo.entitlements.active),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
99
159
|
try {
|
|
100
160
|
const revenueCatData = extractRevenueCatData(event.customerInfo, this.entitlementId);
|
|
101
161
|
revenueCatData.packageType = event.packageType ?? null;
|
|
@@ -105,6 +165,15 @@ export class SubscriptionSyncProcessor {
|
|
|
105
165
|
|
|
106
166
|
const creditsUserId = await this.getCreditsUserId(event.userId);
|
|
107
167
|
|
|
168
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
169
|
+
console.log('[SubscriptionSyncProcessor] 🔵 processPurchase: Calling initializeCredits', {
|
|
170
|
+
creditsUserId,
|
|
171
|
+
purchaseId,
|
|
172
|
+
productId: event.productId,
|
|
173
|
+
revenueCatUserId: revenueCatData.revenueCatUserId,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
108
177
|
const result = await getCreditsRepository().initializeCredits(
|
|
109
178
|
creditsUserId,
|
|
110
179
|
purchaseId,
|
|
@@ -119,6 +188,13 @@ export class SubscriptionSyncProcessor {
|
|
|
119
188
|
}
|
|
120
189
|
|
|
121
190
|
this.emitCreditsUpdated(creditsUserId);
|
|
191
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
192
|
+
console.log('[SubscriptionSyncProcessor] 🟢 processPurchase: Credits initialized successfully', {
|
|
193
|
+
creditsUserId,
|
|
194
|
+
purchaseId,
|
|
195
|
+
credits: result.data?.credits,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
122
198
|
} finally {
|
|
123
199
|
this.purchaseInProgress = false;
|
|
124
200
|
}
|
|
@@ -198,6 +274,15 @@ export class SubscriptionSyncProcessor {
|
|
|
198
274
|
private async syncPremiumStatus(userId: string, event: PremiumStatusChangedEvent): Promise<void> {
|
|
199
275
|
const repo = getCreditsRepository();
|
|
200
276
|
|
|
277
|
+
if (__DEV__) {
|
|
278
|
+
console.log('[SubscriptionSyncProcessor] 🔵 syncPremiumStatus: Starting', {
|
|
279
|
+
userId,
|
|
280
|
+
isPremium: event.isPremium,
|
|
281
|
+
productId: event.productId,
|
|
282
|
+
willRenew: event.willRenew,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
201
286
|
if (event.isPremium) {
|
|
202
287
|
const created = await repo.ensurePremiumCreditsExist(
|
|
203
288
|
userId,
|
|
@@ -208,7 +293,7 @@ export class SubscriptionSyncProcessor {
|
|
|
208
293
|
event.storeTransactionId,
|
|
209
294
|
);
|
|
210
295
|
if (__DEV__ && created) {
|
|
211
|
-
console.log('[SubscriptionSyncProcessor] Recovery: created missing credits document for premium user', {
|
|
296
|
+
console.log('[SubscriptionSyncProcessor] 🟢 Recovery: created missing credits document for premium user', {
|
|
212
297
|
userId,
|
|
213
298
|
productId: event.productId,
|
|
214
299
|
});
|
|
@@ -227,6 +312,14 @@ export class SubscriptionSyncProcessor {
|
|
|
227
312
|
ownershipType: event.ownershipType ?? null,
|
|
228
313
|
});
|
|
229
314
|
this.emitCreditsUpdated(userId);
|
|
315
|
+
|
|
316
|
+
if (__DEV__) {
|
|
317
|
+
console.log('[SubscriptionSyncProcessor] 🟢 syncPremiumStatus: Completed', {
|
|
318
|
+
userId,
|
|
319
|
+
isPremium: event.isPremium,
|
|
320
|
+
productId: event.productId,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
230
323
|
}
|
|
231
324
|
|
|
232
325
|
private emitCreditsUpdated(userId: string): void {
|
|
@@ -57,7 +57,7 @@ async function executeSubscriptionPurchase(
|
|
|
57
57
|
const source = savedPurchase?.source;
|
|
58
58
|
|
|
59
59
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
|
-
console.log("[PurchaseExecutor] executeSubscriptionPurchase:", {
|
|
60
|
+
console.log("[PurchaseExecutor] 🔵 executeSubscriptionPurchase: START", {
|
|
61
61
|
userId,
|
|
62
62
|
productId,
|
|
63
63
|
isPremium,
|
|
@@ -65,13 +65,27 @@ async function executeSubscriptionPurchase(
|
|
|
65
65
|
activeEntitlements: Object.keys(customerInfo.entitlements.active),
|
|
66
66
|
source,
|
|
67
67
|
packageType,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
68
69
|
});
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
try {
|
|
72
73
|
await notifyPurchaseCompleted(config, userId, productId, customerInfo, source, packageType);
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
+
console.log("[PurchaseExecutor] 🟢 executeSubscriptionPurchase: SUCCESS", {
|
|
76
|
+
userId,
|
|
77
|
+
productId,
|
|
78
|
+
isPremium,
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
73
82
|
} catch (syncError) {
|
|
74
|
-
console.error('[PurchaseExecutor] Post-purchase sync failed, attempting recovery:',
|
|
83
|
+
console.error('[PurchaseExecutor] 🔴 Post-purchase sync failed, attempting recovery:', {
|
|
84
|
+
userId,
|
|
85
|
+
productId,
|
|
86
|
+
error: syncError instanceof Error ? syncError.message : String(syncError),
|
|
87
|
+
timestamp: new Date().toISOString(),
|
|
88
|
+
});
|
|
75
89
|
await attemptRecovery(config, userId, customerInfo);
|
|
76
90
|
} finally {
|
|
77
91
|
if (savedPurchase) {
|