@umituz/react-native-subscription 3.1.10 → 3.1.12
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/presentation/useCreditsRealTime.ts +10 -5
- package/src/domains/credits/utils/creditValidation.ts +5 -26
- package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
- package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
- package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
- package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
- package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
- package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
- package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
- package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
- package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
- package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
- package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
- package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
- package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +8 -2
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
- package/src/index.components.ts +1 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
- package/src/shared/presentation/hooks/useFirestoreRealTime.ts +22 -6
- package/src/shared/utils/errors/errorAssertions.ts +35 -0
- package/src/shared/utils/errors/errorConversion.ts +73 -0
- package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
- package/src/shared/utils/errors/errorWrappers.ts +54 -0
- package/src/shared/utils/errors/index.ts +19 -0
- package/src/shared/utils/errors/serviceErrors.ts +36 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
- package/src/shared/utils/errorUtils.ts +0 -195
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.12",
|
|
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",
|
|
@@ -49,15 +49,20 @@ export function useCreditsRealTime(userId: string | null | undefined) {
|
|
|
49
49
|
/**
|
|
50
50
|
* Hook to get derived credit values with real-time sync.
|
|
51
51
|
* This is the real-time equivalent of the computed values in useCredits.
|
|
52
|
+
*
|
|
53
|
+
* PERFORMANCE: Uses useMemo to avoid recalculating on every render
|
|
52
54
|
*/
|
|
53
55
|
export function useCreditsRealTimeDerived(userId: string | null | undefined) {
|
|
54
56
|
const { credits, isLoading } = useCreditsRealTime(userId);
|
|
55
57
|
|
|
56
|
-
const hasCredits = (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const { hasCredits, creditsPercent } = useMemo(() => {
|
|
59
|
+
const hasCredits = (credits?.credits ?? 0) > 0;
|
|
60
|
+
const creditsPercent = credits ? Math.min(
|
|
61
|
+
(credits.credits / credits.creditLimit) * 100,
|
|
62
|
+
100
|
|
63
|
+
) : 0;
|
|
64
|
+
return { hasCredits, creditsPercent };
|
|
65
|
+
}, [credits]); // Include full credits object to catch all changes
|
|
61
66
|
|
|
62
67
|
return {
|
|
63
68
|
hasCredits,
|
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Credit validation utilities
|
|
3
|
+
* Re-exports validation functions from creditCalculations for backwards compatibility
|
|
4
|
+
*/
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
return isValidNumber(balance) && isNonNegativeNumber(balance);
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const isValidCost = (cost: number): boolean => {
|
|
8
|
-
return isValidNumber(cost) && isNonNegativeNumber(cost);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const isValidMaxCredits = (max: number): boolean => {
|
|
12
|
-
return isValidNumber(max) && max > 0;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const canAffordAmount = (balance: number | null | undefined, cost: number): boolean => {
|
|
16
|
-
if (!isValidBalance(balance) || !isValidCost(cost)) return false;
|
|
17
|
-
return balance >= cost;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export const calculateSafePercentage = (
|
|
21
|
-
current: number | null | undefined,
|
|
22
|
-
max: number
|
|
23
|
-
): number => {
|
|
24
|
-
if (!isValidNumber(current) || !isValidMaxCredits(max)) return 0;
|
|
25
|
-
const percentage = (current / max) * 100;
|
|
26
|
-
return Math.min(Math.max(percentage, 0), 100);
|
|
27
|
-
};
|
|
6
|
+
export { canAffordAmount, calculateSafePercentage } from './creditCalculations';
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Paywall Actions Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Main entry point that combines purchase and restore handlers.
|
|
5
|
+
* Handlers extracted to separate modules for better maintainability.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState,
|
|
9
|
-
import type {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
interface UsePaywallActionsParams {
|
|
13
|
-
packages?: PurchasesPackage[];
|
|
14
|
-
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
15
|
-
restorePurchase: () => Promise<boolean>;
|
|
16
|
-
source?: string; // PurchaseSource
|
|
17
|
-
onPurchaseSuccess?: () => void;
|
|
18
|
-
onPurchaseError?: (error: Error | string) => void;
|
|
19
|
-
onAuthRequired?: () => void;
|
|
20
|
-
onClose?: () => void;
|
|
21
|
-
}
|
|
8
|
+
import { useState, useRef, useMemo } from "react";
|
|
9
|
+
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
10
|
+
import { usePurchaseHandler } from "./usePaywallPurchase";
|
|
11
|
+
import { useRestoreHandler } from "./usePaywallRestore";
|
|
22
12
|
|
|
23
13
|
export function usePaywallActions({
|
|
24
14
|
packages = [],
|
|
@@ -35,8 +25,6 @@ export function usePaywallActions({
|
|
|
35
25
|
const isProcessingRef = useRef(isProcessing);
|
|
36
26
|
isProcessingRef.current = isProcessing;
|
|
37
27
|
|
|
38
|
-
const { verifyPremiumStatus } = usePremiumVerification();
|
|
39
|
-
|
|
40
28
|
// Ref management
|
|
41
29
|
const callbacksRef = useRef({
|
|
42
30
|
purchasePackage,
|
|
@@ -59,128 +47,28 @@ export function usePaywallActions({
|
|
|
59
47
|
packages,
|
|
60
48
|
};
|
|
61
49
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!currentSelectedId) return;
|
|
69
|
-
if (isProcessingRef.current) return;
|
|
70
|
-
|
|
71
|
-
const pkg = callbacksRef.current.packages.find((p) => p.product.identifier === currentSelectedId);
|
|
72
|
-
if (!pkg) {
|
|
73
|
-
callbacksRef.current.onPurchaseError?.(new Error(`Package not found: ${currentSelectedId}`));
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (__DEV__) {
|
|
78
|
-
console.log('[usePaywallActions] 🛒 Starting purchase', {
|
|
79
|
-
productId: pkg.product.identifier,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
setIsProcessing(true);
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
const success = await callbacksRef.current.purchasePackage(pkg);
|
|
87
|
-
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log('[usePaywallActions] ✅ Purchase completed', { success });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let isActuallySuccessful = success === true;
|
|
93
|
-
|
|
94
|
-
// Fallback verification if success is undefined
|
|
95
|
-
if (success === undefined) {
|
|
96
|
-
isActuallySuccessful = await verifyPremiumStatus();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isActuallySuccessful) {
|
|
100
|
-
if (__DEV__) {
|
|
101
|
-
console.log('[usePaywallActions] 🎉 Purchase successful, closing paywall');
|
|
102
|
-
}
|
|
103
|
-
callbacksRef.current.onPurchaseSuccess?.();
|
|
104
|
-
callbacksRef.current.onClose?.();
|
|
105
|
-
} else {
|
|
106
|
-
if (__DEV__) {
|
|
107
|
-
console.warn('[usePaywallActions] ⚠️ Purchase did not indicate success');
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (__DEV__) {
|
|
112
|
-
console.error('[usePaywallActions] ❌ Purchase error:', error);
|
|
113
|
-
}
|
|
114
|
-
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
115
|
-
} finally {
|
|
116
|
-
setIsProcessing(false);
|
|
117
|
-
}
|
|
118
|
-
}, [selectedPlanId, verifyPremiumStatus]);
|
|
119
|
-
|
|
120
|
-
// ─────────────────────────────────────────────────────────────
|
|
121
|
-
// RESTORE HANDLER
|
|
122
|
-
// ─────────────────────────────────────────────────────────────
|
|
123
|
-
|
|
124
|
-
const handleRestore = useCallback(async () => {
|
|
125
|
-
if (isProcessingRef.current) return;
|
|
126
|
-
|
|
127
|
-
if (__DEV__) {
|
|
128
|
-
console.log('[usePaywallActions] 🔄 Starting restore');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
setIsProcessing(true);
|
|
132
|
-
|
|
133
|
-
try {
|
|
134
|
-
const success = await callbacksRef.current.restorePurchase();
|
|
135
|
-
|
|
136
|
-
if (__DEV__) {
|
|
137
|
-
console.log('[usePaywallActions] ✅ Restore completed', { success });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let isActuallySuccessful = success === true;
|
|
141
|
-
|
|
142
|
-
// Fallback verification if success is undefined
|
|
143
|
-
if (success === undefined) {
|
|
144
|
-
isActuallySuccessful = await verifyPremiumStatus();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isActuallySuccessful) {
|
|
148
|
-
if (__DEV__) {
|
|
149
|
-
console.log('[usePaywallActions] 🎉 Restore successful, closing paywall');
|
|
150
|
-
}
|
|
151
|
-
callbacksRef.current.onPurchaseSuccess?.();
|
|
152
|
-
callbacksRef.current.onClose?.();
|
|
153
|
-
} else {
|
|
154
|
-
if (__DEV__) {
|
|
155
|
-
console.warn('[usePaywallActions] ⚠️ Restore did not indicate success');
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} catch (error) {
|
|
159
|
-
if (__DEV__) {
|
|
160
|
-
console.error('[usePaywallActions] ❌ Restore error:', error);
|
|
161
|
-
}
|
|
162
|
-
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
163
|
-
} finally {
|
|
164
|
-
setIsProcessing(false);
|
|
165
|
-
}
|
|
166
|
-
}, [verifyPremiumStatus]);
|
|
50
|
+
// Extracted handlers
|
|
51
|
+
const handlePurchase = usePurchaseHandler({
|
|
52
|
+
selectedPlanId,
|
|
53
|
+
isProcessingRef,
|
|
54
|
+
callbacksRef,
|
|
55
|
+
});
|
|
167
56
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
57
|
+
const handleRestore = useRestoreHandler({
|
|
58
|
+
isProcessingRef,
|
|
59
|
+
callbacksRef,
|
|
60
|
+
});
|
|
171
61
|
|
|
172
|
-
|
|
62
|
+
// Reset state
|
|
63
|
+
const resetState = () => {
|
|
173
64
|
if (__DEV__) {
|
|
174
65
|
console.log('[usePaywallActions] 🧹 Resetting state');
|
|
175
66
|
}
|
|
176
67
|
setSelectedPlanId(null);
|
|
177
68
|
setIsProcessing(false);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ─────────────────────────────────────────────────────────────
|
|
181
|
-
// RETURN
|
|
182
|
-
// ─────────────────────────────────────────────────────────────
|
|
69
|
+
};
|
|
183
70
|
|
|
71
|
+
// Return API
|
|
184
72
|
return useMemo(() => ({
|
|
185
73
|
selectedPlanId,
|
|
186
74
|
setSelectedPlanId,
|
|
@@ -188,5 +76,5 @@ export function usePaywallActions({
|
|
|
188
76
|
handlePurchase,
|
|
189
77
|
handleRestore,
|
|
190
78
|
resetState,
|
|
191
|
-
}), [selectedPlanId, isProcessing, handlePurchase, handleRestore
|
|
79
|
+
}), [selectedPlanId, isProcessing, handlePurchase, handleRestore]);
|
|
192
80
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Actions Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
6
|
+
|
|
7
|
+
export interface UsePaywallActionsParams {
|
|
8
|
+
packages?: PurchasesPackage[];
|
|
9
|
+
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
10
|
+
restorePurchase: () => Promise<boolean>;
|
|
11
|
+
source?: string;
|
|
12
|
+
onPurchaseSuccess?: () => void;
|
|
13
|
+
onPurchaseError?: (error: Error | string) => void;
|
|
14
|
+
onAuthRequired?: () => void;
|
|
15
|
+
onClose?: () => void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Purchase Handler
|
|
3
|
+
*
|
|
4
|
+
* Extracted purchase logic from usePaywallActions for better modularity.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback } from "react";
|
|
8
|
+
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
9
|
+
import { usePremiumVerification } from "./usePaywallActions.utils";
|
|
10
|
+
|
|
11
|
+
interface UsePurchaseHandlerParams {
|
|
12
|
+
selectedPlanId: string | null;
|
|
13
|
+
isProcessingRef: React.MutableRefObject<boolean>;
|
|
14
|
+
callbacksRef: React.MutableRefObject<UsePaywallActionsParams>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook to handle purchase operations.
|
|
19
|
+
* Extracted for better testability and modularity.
|
|
20
|
+
*/
|
|
21
|
+
export function usePurchaseHandler({
|
|
22
|
+
selectedPlanId,
|
|
23
|
+
isProcessingRef,
|
|
24
|
+
callbacksRef,
|
|
25
|
+
}: UsePurchaseHandlerParams) {
|
|
26
|
+
const { verifyPremiumStatus } = usePremiumVerification();
|
|
27
|
+
|
|
28
|
+
const handlePurchase = useCallback(async () => {
|
|
29
|
+
const currentSelectedId = selectedPlanId;
|
|
30
|
+
if (!currentSelectedId) return;
|
|
31
|
+
if (isProcessingRef.current) return;
|
|
32
|
+
|
|
33
|
+
const pkg = callbacksRef.current.packages?.find((p) => p.product.identifier === currentSelectedId);
|
|
34
|
+
if (!pkg) {
|
|
35
|
+
callbacksRef.current.onPurchaseError?.(new Error(`Package not found: ${currentSelectedId}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (__DEV__) {
|
|
40
|
+
console.log('[usePurchaseHandler] 🛒 Starting purchase', {
|
|
41
|
+
productId: pkg.product.identifier,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const success = await callbacksRef.current.purchasePackage(pkg);
|
|
47
|
+
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.log('[usePurchaseHandler] ✅ Purchase completed', { success });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let isActuallySuccessful = success === true;
|
|
53
|
+
|
|
54
|
+
if (success === undefined) {
|
|
55
|
+
isActuallySuccessful = await verifyPremiumStatus();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isActuallySuccessful) {
|
|
59
|
+
if (__DEV__) {
|
|
60
|
+
console.log('[usePurchaseHandler] 🎉 Purchase successful, closing paywall');
|
|
61
|
+
}
|
|
62
|
+
callbacksRef.current.onPurchaseSuccess?.();
|
|
63
|
+
callbacksRef.current.onClose?.();
|
|
64
|
+
} else {
|
|
65
|
+
if (__DEV__) {
|
|
66
|
+
console.warn('[usePurchaseHandler] ⚠️ Purchase did not indicate success');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.error('[usePurchaseHandler] ❌ Purchase error:', error);
|
|
72
|
+
}
|
|
73
|
+
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
74
|
+
}
|
|
75
|
+
}, [selectedPlanId, verifyPremiumStatus, callbacksRef, isProcessingRef]);
|
|
76
|
+
|
|
77
|
+
return handlePurchase;
|
|
78
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall Restore Handler
|
|
3
|
+
*
|
|
4
|
+
* Extracted restore logic from usePaywallActions for better modularity.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback } from "react";
|
|
8
|
+
import type { UsePaywallActionsParams } from "./usePaywallActions.types";
|
|
9
|
+
import { usePremiumVerification } from "./usePaywallActions.utils";
|
|
10
|
+
|
|
11
|
+
interface UseRestoreHandlerParams {
|
|
12
|
+
isProcessingRef: React.MutableRefObject<boolean>;
|
|
13
|
+
callbacksRef: React.MutableRefObject<UsePaywallActionsParams>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hook to handle restore operations.
|
|
18
|
+
* Extracted for better testability and modularity.
|
|
19
|
+
*/
|
|
20
|
+
export function useRestoreHandler({
|
|
21
|
+
isProcessingRef,
|
|
22
|
+
callbacksRef,
|
|
23
|
+
}: UseRestoreHandlerParams) {
|
|
24
|
+
const { verifyPremiumStatus } = usePremiumVerification();
|
|
25
|
+
|
|
26
|
+
const handleRestore = useCallback(async () => {
|
|
27
|
+
if (isProcessingRef.current) return;
|
|
28
|
+
|
|
29
|
+
if (__DEV__) {
|
|
30
|
+
console.log('[useRestoreHandler] 🔄 Starting restore');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const success = await callbacksRef.current.restorePurchase();
|
|
35
|
+
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
console.log('[useRestoreHandler] ✅ Restore completed', { success });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let isActuallySuccessful = success === true;
|
|
41
|
+
|
|
42
|
+
if (success === undefined) {
|
|
43
|
+
isActuallySuccessful = await verifyPremiumStatus();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isActuallySuccessful) {
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.log('[useRestoreHandler] 🎉 Restore successful, closing paywall');
|
|
49
|
+
}
|
|
50
|
+
callbacksRef.current.onPurchaseSuccess?.();
|
|
51
|
+
callbacksRef.current.onClose?.();
|
|
52
|
+
} else {
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
console.warn('[useRestoreHandler] ⚠️ Restore did not indicate success');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.error('[useRestoreHandler] ❌ Restore error:', error);
|
|
60
|
+
}
|
|
61
|
+
callbacksRef.current.onPurchaseError?.(error instanceof Error ? error : new Error(String(error)));
|
|
62
|
+
}
|
|
63
|
+
}, [verifyPremiumStatus, callbacksRef, isProcessingRef]);
|
|
64
|
+
|
|
65
|
+
return handleRestore;
|
|
66
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Switch Core Handler
|
|
3
|
+
*
|
|
4
|
+
* Core user switch logic. Handles switching between anonymous and authenticated users.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Purchases, { type CustomerInfo } from "react-native-purchases";
|
|
8
|
+
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
9
|
+
import type { InitializerDeps } from "./RevenueCatInitializer.types";
|
|
10
|
+
import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
|
|
11
|
+
import { UserSwitchMutex } from "./UserSwitchMutex";
|
|
12
|
+
import { ANONYMOUS_CACHE_KEY } from "../../../subscription/core/SubscriptionConstants";
|
|
13
|
+
import {
|
|
14
|
+
normalizeUserId,
|
|
15
|
+
isAnonymousId,
|
|
16
|
+
buildSuccessResult,
|
|
17
|
+
fetchOfferingsSafe,
|
|
18
|
+
} from "./userSwitchHelpers";
|
|
19
|
+
|
|
20
|
+
declare const __DEV__: boolean;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Handle user switch operation with mutex protection.
|
|
24
|
+
*/
|
|
25
|
+
export async function handleUserSwitch(
|
|
26
|
+
deps: InitializerDeps,
|
|
27
|
+
userId: string
|
|
28
|
+
): Promise<InitializeResult> {
|
|
29
|
+
const mutexKey = userId || ANONYMOUS_CACHE_KEY;
|
|
30
|
+
|
|
31
|
+
const { shouldProceed, existingPromise } = await UserSwitchMutex.acquire(mutexKey);
|
|
32
|
+
|
|
33
|
+
if (!shouldProceed && existingPromise) {
|
|
34
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
35
|
+
console.log('[UserSwitchCore] Using result from active switch operation');
|
|
36
|
+
}
|
|
37
|
+
return existingPromise as Promise<InitializeResult>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const switchOperation = performUserSwitch(deps, userId);
|
|
41
|
+
UserSwitchMutex.setPromise(switchOperation);
|
|
42
|
+
return switchOperation;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Perform the actual user switch operation.
|
|
47
|
+
*/
|
|
48
|
+
async function performUserSwitch(
|
|
49
|
+
deps: InitializerDeps,
|
|
50
|
+
userId: string
|
|
51
|
+
): Promise<InitializeResult> {
|
|
52
|
+
try {
|
|
53
|
+
const currentAppUserId = await Purchases.getAppUserID();
|
|
54
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
55
|
+
const normalizedCurrentUserId = isAnonymousId(currentAppUserId) ? null : currentAppUserId;
|
|
56
|
+
|
|
57
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
58
|
+
console.log('[UserSwitchCore] performUserSwitch:', {
|
|
59
|
+
providedUserId: userId,
|
|
60
|
+
normalizedUserId: normalizedUserId || '(null - anonymous)',
|
|
61
|
+
currentAppUserId,
|
|
62
|
+
normalizedCurrentUserId: normalizedCurrentUserId || '(null - anonymous)',
|
|
63
|
+
needsSwitch: normalizedCurrentUserId !== normalizedUserId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let customerInfo: CustomerInfo;
|
|
68
|
+
|
|
69
|
+
if (normalizedCurrentUserId !== normalizedUserId) {
|
|
70
|
+
if (normalizedUserId) {
|
|
71
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
72
|
+
console.log('[UserSwitchCore] Calling Purchases.logIn() to switch from anonymous to:', normalizedUserId);
|
|
73
|
+
}
|
|
74
|
+
const result = await Purchases.logIn(normalizedUserId!);
|
|
75
|
+
customerInfo = result.customerInfo;
|
|
76
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
77
|
+
console.log('[UserSwitchCore] Purchases.logIn() successful, created:', result.created);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
81
|
+
console.log('[UserSwitchCore] User is anonymous, fetching customer info');
|
|
82
|
+
}
|
|
83
|
+
customerInfo = await Purchases.getCustomerInfo();
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
87
|
+
console.log('[UserSwitchCore] No user switch needed, fetching current customer info');
|
|
88
|
+
}
|
|
89
|
+
customerInfo = await Purchases.getCustomerInfo();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
deps.setInitialized(true);
|
|
93
|
+
deps.setCurrentUserId(normalizedUserId || undefined);
|
|
94
|
+
const offerings = await fetchOfferingsSafe();
|
|
95
|
+
|
|
96
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
97
|
+
console.log('[UserSwitchCore] User switch completed successfully');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return buildSuccessResult(deps, customerInfo, offerings);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
let currentAppUserId = 'unknown';
|
|
103
|
+
try {
|
|
104
|
+
currentAppUserId = await Purchases.getAppUserID();
|
|
105
|
+
} catch {
|
|
106
|
+
// Ignore error in error handler
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.error('[UserSwitchCore] Failed during user switch or fetch', {
|
|
110
|
+
userId,
|
|
111
|
+
currentAppUserId,
|
|
112
|
+
error
|
|
113
|
+
});
|
|
114
|
+
return FAILED_INITIALIZATION_RESULT;
|
|
115
|
+
}
|
|
116
|
+
}
|