@umituz/react-native-subscription 3.1.34 → 3.1.35
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 +34 -39
- package/src/domains/credits/application/DeductCreditsCommand.ts +13 -12
- package/src/domains/credits/presentation/useCredits.types.ts +1 -1
- package/src/domains/paywall/components/PaywallScreen.tsx +12 -11
- package/src/domains/paywall/hooks/usePaywallActions.ts +4 -3
- package/src/domains/paywall/hooks/usePaywallActions.utils.ts +14 -19
- package/src/domains/paywall/hooks/usePaywallPurchase.ts +10 -17
- package/src/domains/paywall/hooks/usePaywallRestore.ts +8 -15
- package/src/domains/revenuecat/infrastructure/services/RevenueCatInitializer.ts +6 -5
- package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +8 -10
- package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +16 -33
- package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +3 -4
- package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +18 -28
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +17 -29
- package/src/domains/subscription/application/sync/CreditDocumentOperations.ts +16 -17
- package/src/domains/subscription/application/sync/PurchaseSyncHandler.ts +20 -23
- package/src/domains/subscription/application/sync/RenewalSyncHandler.ts +8 -7
- package/src/domains/subscription/application/sync/StatusChangeSyncHandler.ts +4 -3
- package/src/domains/subscription/application/sync/SyncProcessorLogger.ts +39 -64
- package/src/domains/subscription/application/sync/UserIdResolver.ts +5 -1
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +7 -6
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +8 -7
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +4 -3
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +20 -27
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +10 -9
- package/src/domains/subscription/infrastructure/services/OfferingsFetcher.ts +14 -21
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +8 -7
- package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +4 -3
- package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +15 -29
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseErrorHandler.ts +4 -2
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +27 -33
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +5 -1
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +11 -17
- package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +11 -12
- package/src/domains/subscription/presentation/useSyncStatusListener.ts +10 -9
- package/src/init/createSubscriptionInitModule.ts +4 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.35",
|
|
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",
|
|
@@ -9,6 +9,9 @@ import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
|
|
|
9
9
|
import { CreditLimitService } from "../domain/services/CreditLimitService";
|
|
10
10
|
import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
|
|
11
11
|
import { PURCHASE_ID_PREFIXES } from "../core/CreditsConstants";
|
|
12
|
+
import { createLogger } from "../../../shared/utils/logger";
|
|
13
|
+
|
|
14
|
+
const logger = createLogger("CreditsInitializer");
|
|
12
15
|
|
|
13
16
|
export async function initializeCreditsTransaction(
|
|
14
17
|
_db: Firestore,
|
|
@@ -19,19 +22,17 @@ export async function initializeCreditsTransaction(
|
|
|
19
22
|
userId: string
|
|
20
23
|
): Promise<InitializationResult> {
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
});
|
|
34
|
-
}
|
|
25
|
+
logger.debug("initializeCreditsTransaction: START", {
|
|
26
|
+
userId,
|
|
27
|
+
purchaseId,
|
|
28
|
+
productId: metadata.productId,
|
|
29
|
+
source: metadata.source,
|
|
30
|
+
type: metadata.type,
|
|
31
|
+
isPremium: metadata.isPremium,
|
|
32
|
+
willRenew: metadata.willRenew,
|
|
33
|
+
storeTransactionId: metadata.storeTransactionId,
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
});
|
|
35
36
|
|
|
36
37
|
if (!purchaseId.startsWith(PURCHASE_ID_PREFIXES.PURCHASE) && !purchaseId.startsWith(PURCHASE_ID_PREFIXES.RENEWAL)) {
|
|
37
38
|
throw new Error(`[CreditsInitializer] Only purchase and renewal operations can allocate credits. Received: ${purchaseId}`);
|
|
@@ -46,14 +47,12 @@ export async function initializeCreditsTransaction(
|
|
|
46
47
|
const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
|
|
47
48
|
|
|
48
49
|
if (existingData.processedPurchases.includes(purchaseId)) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
}
|
|
50
|
+
logger.debug("Transaction already processed, skipping", {
|
|
51
|
+
userId,
|
|
52
|
+
purchaseId,
|
|
53
|
+
existingCredits: existingData.credits,
|
|
54
|
+
processedPurchasesCount: existingData.processedPurchases.length,
|
|
55
|
+
});
|
|
57
56
|
return {
|
|
58
57
|
credits: existingData.credits,
|
|
59
58
|
alreadyProcessed: true,
|
|
@@ -61,14 +60,12 @@ export async function initializeCreditsTransaction(
|
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
}
|
|
63
|
+
logger.debug("Processing credit allocation", {
|
|
64
|
+
userId,
|
|
65
|
+
purchaseId,
|
|
66
|
+
existingCredits: existingData.credits,
|
|
67
|
+
productId: metadata.productId,
|
|
68
|
+
});
|
|
72
69
|
|
|
73
70
|
const creditLimitService = new CreditLimitService(config);
|
|
74
71
|
const creditLimit = creditLimitService.calculate(metadata.productId);
|
|
@@ -100,16 +97,14 @@ export async function initializeCreditsTransaction(
|
|
|
100
97
|
|
|
101
98
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
102
99
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
}
|
|
100
|
+
logger.debug("Credit allocation successful", {
|
|
101
|
+
userId,
|
|
102
|
+
purchaseId,
|
|
103
|
+
previousCredits: existingData.credits,
|
|
104
|
+
newCredits,
|
|
105
|
+
creditLimit,
|
|
106
|
+
productId: metadata.productId,
|
|
107
|
+
});
|
|
113
108
|
|
|
114
109
|
return {
|
|
115
110
|
credits: newCredits,
|
|
@@ -2,6 +2,9 @@ import { runTransaction, serverTimestamp, type DocumentReference } from "firebas
|
|
|
2
2
|
import type { Firestore } from "@umituz/react-native-firebase";
|
|
3
3
|
import type { DeductCreditsResult } from "../core/Credits";
|
|
4
4
|
import { CREDIT_ERROR_CODES, MAX_SINGLE_DEDUCTION } from "../core/CreditsConstants";
|
|
5
|
+
import { createLogger } from "../../../shared/utils/logger";
|
|
6
|
+
|
|
7
|
+
const logger = createLogger("DeductCreditsCommand");
|
|
5
8
|
|
|
6
9
|
export async function deductCreditsOperation(
|
|
7
10
|
_db: Firestore,
|
|
@@ -32,12 +35,12 @@ export async function deductCreditsOperation(
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
try {
|
|
35
|
-
|
|
38
|
+
logger.debug(">>> starting transaction", { userId, cost, creditsRefPath: creditsRef.path });
|
|
36
39
|
|
|
37
40
|
const remaining = await runTransaction(_db, async (tx) => {
|
|
38
41
|
const docSnap = await tx.get(creditsRef);
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
logger.debug("doc exists", { exists: docSnap.exists() });
|
|
41
44
|
|
|
42
45
|
if (!docSnap.exists()) {
|
|
43
46
|
throw new Error(CREDIT_ERROR_CODES.NO_CREDITS);
|
|
@@ -46,7 +49,7 @@ export async function deductCreditsOperation(
|
|
|
46
49
|
const rawCredits = docSnap.data().credits;
|
|
47
50
|
const current = typeof rawCredits === "number" && Number.isFinite(rawCredits) ? rawCredits : 0;
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
logger.debug("current credits", { current, cost });
|
|
50
53
|
|
|
51
54
|
if (current < cost) {
|
|
52
55
|
throw new Error(CREDIT_ERROR_CODES.CREDITS_EXHAUSTED);
|
|
@@ -58,12 +61,12 @@ export async function deductCreditsOperation(
|
|
|
58
61
|
lastUpdatedAt: serverTimestamp()
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
logger.debug("updated credits to", { updated });
|
|
62
65
|
|
|
63
66
|
return updated;
|
|
64
67
|
});
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
logger.debug("transaction SUCCESS", { remaining });
|
|
67
70
|
|
|
68
71
|
return {
|
|
69
72
|
success: true,
|
|
@@ -76,13 +79,11 @@ export async function deductCreditsOperation(
|
|
|
76
79
|
? message
|
|
77
80
|
: CREDIT_ERROR_CODES.DEDUCT_ERR;
|
|
78
81
|
|
|
79
|
-
// Use
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.error('[DeductCreditsCommand] ❌ Unexpected transaction error:', { code, message });
|
|
85
|
-
}
|
|
82
|
+
// Use logger.warn instead of logger.error for "no credits" - this is expected behavior, not a system error
|
|
83
|
+
if (code === CREDIT_ERROR_CODES.NO_CREDITS || code === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED) {
|
|
84
|
+
logger.warn("User has no credits - paywall should open", { code, message });
|
|
85
|
+
} else {
|
|
86
|
+
logger.error("Unexpected transaction error", e, { code, message });
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
return {
|
|
@@ -29,6 +29,9 @@ import {
|
|
|
29
29
|
} from "../utils/paywallLayoutUtils";
|
|
30
30
|
import { hasItems } from "../../../shared/utils/arrayUtils";
|
|
31
31
|
import { PaywallRenderItem } from "./PaywallScreen.renderItem";
|
|
32
|
+
import { createLogger } from "../../../../shared/utils/logger";
|
|
33
|
+
|
|
34
|
+
const logger = createLogger("PaywallScreen");
|
|
32
35
|
|
|
33
36
|
// Paywall layout constants
|
|
34
37
|
const PAYWALL_HEADER_HEIGHT = 60; // Close button + spacing
|
|
@@ -37,13 +40,11 @@ const PAYWALL_FOOTER_HEIGHT_BASE = 280; // Base footer height
|
|
|
37
40
|
export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) => {
|
|
38
41
|
const navigation = useNavigation();
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
}
|
|
43
|
+
logger.debug("Rendering PaywallScreen", {
|
|
44
|
+
hasPackages: !!props.packages?.length,
|
|
45
|
+
packagesCount: props.packages?.length || 0,
|
|
46
|
+
isPremium: props.isPremium,
|
|
47
|
+
});
|
|
47
48
|
|
|
48
49
|
const {
|
|
49
50
|
translations,
|
|
@@ -69,7 +70,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
69
70
|
const responsive = useResponsive();
|
|
70
71
|
|
|
71
72
|
const handleClose = useCallback(() => {
|
|
72
|
-
|
|
73
|
+
logger.debug("Closing paywall");
|
|
73
74
|
if (onClose) {
|
|
74
75
|
onClose();
|
|
75
76
|
} else if (navigation.canGoBack()) {
|
|
@@ -98,7 +99,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
98
99
|
// Cleanup on unmount only - resetState is stable from useCallback
|
|
99
100
|
useEffect(() => {
|
|
100
101
|
return () => {
|
|
101
|
-
|
|
102
|
+
logger.debug("Cleanup: resetting state");
|
|
102
103
|
resetState();
|
|
103
104
|
};
|
|
104
105
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -118,7 +119,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
118
119
|
try {
|
|
119
120
|
if (await Linking.canOpenURL(url)) await Linking.openURL(url);
|
|
120
121
|
} catch (error) {
|
|
121
|
-
|
|
122
|
+
logger.error("Failed to open URL", error);
|
|
122
123
|
}
|
|
123
124
|
}, []);
|
|
124
125
|
|
|
@@ -176,7 +177,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
|
|
|
176
177
|
|
|
177
178
|
// Defensive check for translations
|
|
178
179
|
if (!translations) {
|
|
179
|
-
|
|
180
|
+
logger.warn("Translations prop is missing");
|
|
180
181
|
return null;
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -9,6 +9,9 @@ import { useState, useRef, useMemo, useCallback } from "react";
|
|
|
9
9
|
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
10
10
|
import { usePurchaseHandler } from "./usePaywallPurchase";
|
|
11
11
|
import { useRestoreHandler } from "./usePaywallRestore";
|
|
12
|
+
import { createLogger } from "../../../../shared/utils/logger";
|
|
13
|
+
|
|
14
|
+
const logger = createLogger("usePaywallActions");
|
|
12
15
|
|
|
13
16
|
export function usePaywallActions({
|
|
14
17
|
packages = [],
|
|
@@ -61,9 +64,7 @@ export function usePaywallActions({
|
|
|
61
64
|
|
|
62
65
|
// Reset state - memoized with useCallback to prevent infinite loops
|
|
63
66
|
const resetState = useCallback(() => {
|
|
64
|
-
|
|
65
|
-
console.log('[usePaywallActions] 🧹 Resetting state');
|
|
66
|
-
}
|
|
67
|
+
logger.debug("Resetting state");
|
|
67
68
|
setSelectedPlanId(null);
|
|
68
69
|
setIsProcessing(false);
|
|
69
70
|
}, []);
|
|
@@ -5,32 +5,27 @@
|
|
|
5
5
|
|
|
6
6
|
import { useSubscriptionStatus } from "../../subscription/presentation/useSubscriptionStatus";
|
|
7
7
|
import { useCredits } from "../../credits/presentation/useCredits";
|
|
8
|
+
import { createLogger } from "../../../../shared/utils/logger";
|
|
9
|
+
|
|
10
|
+
const logger = createLogger("PremiumVerification");
|
|
8
11
|
|
|
9
12
|
export function usePremiumVerification() {
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
13
|
+
const { isPremium: isSubscriptionPremium } = useSubscriptionStatus();
|
|
14
|
+
const { credits } = useCredits();
|
|
12
15
|
|
|
13
16
|
const verifyPremiumStatus = async (): Promise<boolean> => {
|
|
14
|
-
|
|
15
|
-
console.log('[PremiumVerification] 🔍 Checking premium status as fallback...');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const [statusResult, creditsResult] = await Promise.all([
|
|
19
|
-
refetchStatus(),
|
|
20
|
-
refetchCredits()
|
|
21
|
-
]);
|
|
17
|
+
logger.debug("Checking premium status as fallback...");
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// With real-time sync, data is already up-to-date via onSnapshot
|
|
20
|
+
// No need to manually refetch - just check current state
|
|
21
|
+
const isCreditsPremium = credits?.isPremium ?? false;
|
|
25
22
|
const isActuallySuccessful = isSubscriptionPremium || isCreditsPremium;
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
}
|
|
24
|
+
logger.debug("Fallback check result", {
|
|
25
|
+
isSubscriptionPremium,
|
|
26
|
+
isCreditsPremium,
|
|
27
|
+
isActuallySuccessful
|
|
28
|
+
});
|
|
34
29
|
|
|
35
30
|
return isActuallySuccessful;
|
|
36
31
|
};
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import { useCallback } from "react";
|
|
8
8
|
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
9
9
|
import { usePremiumVerification } from "./usePaywallActions.utils";
|
|
10
|
+
import { createLogger } from "../../../../shared/utils/logger";
|
|
11
|
+
|
|
12
|
+
const logger = createLogger("usePurchaseHandler");
|
|
10
13
|
|
|
11
14
|
interface UsePurchaseHandlerParams {
|
|
12
15
|
selectedPlanId: string | null;
|
|
@@ -36,18 +39,14 @@ export function usePurchaseHandler({
|
|
|
36
39
|
return;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
43
|
-
}
|
|
42
|
+
logger.debug("Starting purchase", {
|
|
43
|
+
productId: pkg.product.identifier,
|
|
44
|
+
});
|
|
44
45
|
|
|
45
46
|
try {
|
|
46
47
|
const success = await callbacksRef.current.purchasePackage(pkg);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
console.log('[usePurchaseHandler] ✅ Purchase completed', { success });
|
|
50
|
-
}
|
|
49
|
+
logger.debug("Purchase completed", { success });
|
|
51
50
|
|
|
52
51
|
let isActuallySuccessful = success === true;
|
|
53
52
|
|
|
@@ -56,20 +55,14 @@ export function usePurchaseHandler({
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
if (isActuallySuccessful) {
|
|
59
|
-
|
|
60
|
-
console.log('[usePurchaseHandler] 🎉 Purchase successful, closing paywall');
|
|
61
|
-
}
|
|
58
|
+
logger.debug("Purchase successful, closing paywall");
|
|
62
59
|
callbacksRef.current.onPurchaseSuccess?.();
|
|
63
60
|
callbacksRef.current.onClose?.();
|
|
64
61
|
} else {
|
|
65
|
-
|
|
66
|
-
console.warn('[usePurchaseHandler] ⚠️ Purchase did not indicate success');
|
|
67
|
-
}
|
|
62
|
+
logger.warn("Purchase did not indicate success");
|
|
68
63
|
}
|
|
69
64
|
} catch (error) {
|
|
70
|
-
|
|
71
|
-
console.error('[usePurchaseHandler] ❌ Purchase error:', error);
|
|
72
|
-
}
|
|
65
|
+
logger.error("Purchase error", error);
|
|
73
66
|
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
74
67
|
}
|
|
75
68
|
}, [selectedPlanId, verifyPremiumStatus, callbacksRef, isProcessingRef]);
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import { useCallback } from "react";
|
|
8
8
|
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
9
9
|
import { usePremiumVerification } from "./usePaywallActions.utils";
|
|
10
|
+
import { createLogger } from "../../../../shared/utils/logger";
|
|
11
|
+
|
|
12
|
+
const logger = createLogger("useRestoreHandler");
|
|
10
13
|
|
|
11
14
|
interface UseRestoreHandlerParams {
|
|
12
15
|
isProcessingRef: React.MutableRefObject<boolean>;
|
|
@@ -26,16 +29,12 @@ export function useRestoreHandler({
|
|
|
26
29
|
const handleRestore = useCallback(async () => {
|
|
27
30
|
if (isProcessingRef.current) return;
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
console.log('[useRestoreHandler] 🔄 Starting restore');
|
|
31
|
-
}
|
|
32
|
+
logger.debug("Starting restore");
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
35
|
const success = await callbacksRef.current.restorePurchase();
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
console.log('[useRestoreHandler] ✅ Restore completed', { success });
|
|
38
|
-
}
|
|
37
|
+
logger.debug("Restore completed", { success });
|
|
39
38
|
|
|
40
39
|
let isActuallySuccessful = success === true;
|
|
41
40
|
|
|
@@ -44,20 +43,14 @@ export function useRestoreHandler({
|
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
if (isActuallySuccessful) {
|
|
47
|
-
|
|
48
|
-
console.log('[useRestoreHandler] 🎉 Restore successful, closing paywall');
|
|
49
|
-
}
|
|
46
|
+
logger.debug("Restore successful, closing paywall");
|
|
50
47
|
callbacksRef.current.onPurchaseSuccess?.();
|
|
51
48
|
callbacksRef.current.onClose?.();
|
|
52
49
|
} else {
|
|
53
|
-
|
|
54
|
-
console.warn('[useRestoreHandler] ⚠️ Restore did not indicate success');
|
|
55
|
-
}
|
|
50
|
+
logger.warn("Restore did not indicate success");
|
|
56
51
|
}
|
|
57
52
|
} catch (error) {
|
|
58
|
-
|
|
59
|
-
console.error('[useRestoreHandler] ❌ Restore error:', error);
|
|
60
|
-
}
|
|
53
|
+
logger.error("Restore error", error);
|
|
61
54
|
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
62
55
|
}
|
|
63
56
|
}, [verifyPremiumStatus, callbacksRef, isProcessingRef]);
|
|
@@ -3,6 +3,9 @@ import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
|
3
3
|
import { FAILED_INITIALIZATION_RESULT, CONFIGURATION_RETRY_DELAY_MS, MAX_INIT_RETRIES } from "./initializerConstants";
|
|
4
4
|
import { configState } from "./ConfigurationStateManager";
|
|
5
5
|
import { handleUserSwitch, handleInitialConfiguration, fetchCurrentUserData } from "./userSwitchHandler";
|
|
6
|
+
import { createLogger } from "../../../../../shared/utils/logger";
|
|
7
|
+
|
|
8
|
+
const logger = createLogger("RevenueCatInitializer");
|
|
6
9
|
|
|
7
10
|
const MAX_CONFIG_START_RETRIES = 3;
|
|
8
11
|
|
|
@@ -32,7 +35,7 @@ export async function initializeSDK(
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
if (configState.isConfiguring) {
|
|
35
|
-
|
|
38
|
+
logger.error("Max retry attempts reached", undefined, { userId });
|
|
36
39
|
return FAILED_INITIALIZATION_RESULT;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -49,18 +52,16 @@ export async function initializeSDK(
|
|
|
49
52
|
resolveConfig = configState.startConfiguration();
|
|
50
53
|
} catch (error) {
|
|
51
54
|
if (configStartRetryCount >= MAX_CONFIG_START_RETRIES) {
|
|
52
|
-
|
|
55
|
+
logger.error("Max configuration start retries reached", error, {
|
|
53
56
|
userId,
|
|
54
57
|
retryCount: configStartRetryCount,
|
|
55
|
-
error
|
|
56
58
|
});
|
|
57
59
|
return FAILED_INITIALIZATION_RESULT;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
logger.error("Failed to start configuration, retrying", error, {
|
|
61
63
|
userId,
|
|
62
64
|
retryCount: configStartRetryCount,
|
|
63
|
-
error
|
|
64
65
|
});
|
|
65
66
|
await new Promise<void>(resolve => setTimeout(() => resolve(), CONFIGURATION_RETRY_DELAY_MS));
|
|
66
67
|
return initializeSDK(deps, userId, apiKey, configStartRetryCount + 1);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from "../../../../../shared/utils/logger";
|
|
2
|
+
|
|
3
|
+
const logger = createLogger("UserSwitchMutex");
|
|
4
|
+
|
|
1
5
|
class UserSwitchMutexImpl {
|
|
2
6
|
private activeSwitchPromise: Promise<unknown> | null = null;
|
|
3
7
|
private activeUserId: string | null = null;
|
|
@@ -6,30 +10,24 @@ class UserSwitchMutexImpl {
|
|
|
6
10
|
|
|
7
11
|
async acquire(userId: string): Promise<{ shouldProceed: boolean; existingPromise?: Promise<unknown> }> {
|
|
8
12
|
if (this.activeSwitchPromise && this.activeUserId === userId) {
|
|
9
|
-
|
|
10
|
-
console.log('[UserSwitchMutex] Switch already in progress for this user, returning existing promise');
|
|
11
|
-
}
|
|
13
|
+
logger.debug("Switch already in progress for this user, returning existing promise");
|
|
12
14
|
return { shouldProceed: false, existingPromise: this.activeSwitchPromise };
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
if (this.activeSwitchPromise) {
|
|
16
|
-
|
|
17
|
-
console.log('[UserSwitchMutex] Waiting for active switch to complete...');
|
|
18
|
-
}
|
|
18
|
+
logger.debug("Waiting for active switch to complete...");
|
|
19
19
|
try {
|
|
20
20
|
await this.activeSwitchPromise;
|
|
21
21
|
} catch (error) {
|
|
22
22
|
// Previous switch failed — this is non-fatal for the current switch,
|
|
23
23
|
// but worth logging so the failure is visible in diagnostics.
|
|
24
|
-
|
|
24
|
+
logger.warn("Previous user switch failed", error);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const timeSinceLastSwitch = Date.now() - this.lastSwitchTime;
|
|
28
28
|
if (timeSinceLastSwitch < this.MIN_SWITCH_INTERVAL_MS) {
|
|
29
29
|
const delayNeeded = this.MIN_SWITCH_INTERVAL_MS - timeSinceLastSwitch;
|
|
30
|
-
|
|
31
|
-
console.log(`[UserSwitchMutex] Rate limiting: waiting ${delayNeeded}ms`);
|
|
32
|
-
}
|
|
30
|
+
logger.debug(`Rate limiting: waiting ${delayNeeded}ms`);
|
|
33
31
|
await new Promise<void>(resolve => setTimeout(() => resolve(), delayNeeded));
|
|
34
32
|
}
|
|
35
33
|
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
buildSuccessResult,
|
|
17
17
|
fetchOfferingsSafe,
|
|
18
18
|
} from "./userSwitchHelpers";
|
|
19
|
+
import { createLogger } from "../../../../../shared/utils/logger";
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
const logger = createLogger("UserSwitchCore");
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Handle user switch operation with mutex protection.
|
|
@@ -31,9 +32,7 @@ export async function handleUserSwitch(
|
|
|
31
32
|
const { shouldProceed, existingPromise } = await UserSwitchMutex.acquire(mutexKey);
|
|
32
33
|
|
|
33
34
|
if (!shouldProceed && existingPromise) {
|
|
34
|
-
|
|
35
|
-
console.log('[UserSwitchCore] Using result from active switch operation');
|
|
36
|
-
}
|
|
35
|
+
logger.debug("Using result from active switch operation");
|
|
37
36
|
return existingPromise as Promise<InitializeResult>;
|
|
38
37
|
}
|
|
39
38
|
|
|
@@ -54,38 +53,28 @@ async function performUserSwitch(
|
|
|
54
53
|
const normalizedUserId = normalizeUserId(userId);
|
|
55
54
|
const normalizedCurrentUserId = isAnonymousId(currentAppUserId) ? null : currentAppUserId;
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
}
|
|
56
|
+
logger.debug("performUserSwitch", {
|
|
57
|
+
providedUserId: userId,
|
|
58
|
+
normalizedUserId: normalizedUserId || '(null - anonymous)',
|
|
59
|
+
currentAppUserId,
|
|
60
|
+
normalizedCurrentUserId: normalizedCurrentUserId || '(null - anonymous)',
|
|
61
|
+
needsSwitch: normalizedCurrentUserId !== normalizedUserId,
|
|
62
|
+
});
|
|
66
63
|
|
|
67
64
|
let customerInfo: CustomerInfo;
|
|
68
65
|
|
|
69
66
|
if (normalizedCurrentUserId !== normalizedUserId) {
|
|
70
67
|
if (normalizedUserId) {
|
|
71
|
-
|
|
72
|
-
console.log('[UserSwitchCore] Calling Purchases.logIn() to switch from anonymous to:', normalizedUserId);
|
|
73
|
-
}
|
|
68
|
+
logger.debug("Calling Purchases.logIn() to switch from anonymous to", normalizedUserId);
|
|
74
69
|
const result = await Purchases.logIn(normalizedUserId!);
|
|
75
70
|
customerInfo = result.customerInfo;
|
|
76
|
-
|
|
77
|
-
console.log('[UserSwitchCore] Purchases.logIn() successful, created:', result.created);
|
|
78
|
-
}
|
|
71
|
+
logger.debug("Purchases.logIn() successful, created", result.created);
|
|
79
72
|
} else {
|
|
80
|
-
|
|
81
|
-
console.log('[UserSwitchCore] User is anonymous, fetching customer info');
|
|
82
|
-
}
|
|
73
|
+
logger.debug("User is anonymous, fetching customer info");
|
|
83
74
|
customerInfo = await Purchases.getCustomerInfo();
|
|
84
75
|
}
|
|
85
76
|
} else {
|
|
86
|
-
|
|
87
|
-
console.log('[UserSwitchCore] No user switch needed, fetching current customer info');
|
|
88
|
-
}
|
|
77
|
+
logger.debug("No user switch needed, fetching current customer info");
|
|
89
78
|
customerInfo = await Purchases.getCustomerInfo();
|
|
90
79
|
}
|
|
91
80
|
|
|
@@ -93,9 +82,7 @@ async function performUserSwitch(
|
|
|
93
82
|
deps.setCurrentUserId(normalizedUserId || undefined);
|
|
94
83
|
const offerings = await fetchOfferingsSafe();
|
|
95
84
|
|
|
96
|
-
|
|
97
|
-
console.log('[UserSwitchCore] User switch completed successfully');
|
|
98
|
-
}
|
|
85
|
+
logger.debug("User switch completed successfully");
|
|
99
86
|
|
|
100
87
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
101
88
|
} catch (error) {
|
|
@@ -106,11 +93,7 @@ async function performUserSwitch(
|
|
|
106
93
|
// Ignore error in error handler
|
|
107
94
|
}
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
userId,
|
|
111
|
-
currentAppUserId,
|
|
112
|
-
error
|
|
113
|
-
});
|
|
96
|
+
logger.error("Failed during user switch or fetch", error, { userId, currentAppUserId });
|
|
114
97
|
return FAILED_INITIALIZATION_RESULT;
|
|
115
98
|
}
|
|
116
99
|
}
|
|
@@ -8,8 +8,9 @@ import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-nat
|
|
|
8
8
|
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
9
9
|
import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
10
10
|
import { ANONYMOUS_CACHE_KEY } from "../../../subscription/core/SubscriptionConstants";
|
|
11
|
+
import { createLogger } from "../../../../../shared/utils/logger";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
const logger = createLogger("UserSwitchHelpers");
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Normalize user ID to null if empty or anonymous cache key.
|
|
@@ -47,9 +48,7 @@ export async function fetchOfferingsSafe(): Promise<PurchasesOfferings | null> {
|
|
|
47
48
|
try {
|
|
48
49
|
return await Purchases.getOfferings();
|
|
49
50
|
} catch (error) {
|
|
50
|
-
|
|
51
|
-
console.warn('[UserSwitchHelpers] Offerings fetch failed (non-fatal):', error);
|
|
52
|
-
}
|
|
51
|
+
logger.warn("Offerings fetch failed (non-fatal)", error);
|
|
53
52
|
return null;
|
|
54
53
|
}
|
|
55
54
|
}
|