@umituz/react-native-subscription 3.1.9 → 3.1.11

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/presentation/useCreditsRealTime.ts +31 -73
  3. package/src/domains/credits/utils/creditValidation.ts +5 -26
  4. package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
  5. package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
  6. package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
  7. package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
  8. package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
  9. package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
  10. package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
  11. package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
  12. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
  13. package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
  14. package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
  15. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
  16. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
  17. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
  18. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
  19. package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
  20. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
  21. package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
  22. package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
  23. package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
  24. package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
  25. package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
  26. package/src/domains/subscription/presentation/flowInitialState.ts +22 -0
  27. package/src/domains/subscription/presentation/flowTypes.ts +106 -0
  28. package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
  29. package/src/domains/subscription/presentation/usePremiumActions.ts +5 -6
  30. package/src/domains/subscription/presentation/useSubscriptionFlow.ts +25 -92
  31. package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
  32. package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
  33. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +34 -60
  34. package/src/index.components.ts +1 -1
  35. package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
  36. package/src/shared/presentation/hooks/useFirestoreRealTime.ts +230 -0
  37. package/src/shared/presentation/types/hookState.types.ts +97 -0
  38. package/src/shared/utils/errors/errorAssertions.ts +35 -0
  39. package/src/shared/utils/errors/errorConversion.ts +73 -0
  40. package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
  41. package/src/shared/utils/errors/errorWrappers.ts +54 -0
  42. package/src/shared/utils/errors/index.ts +19 -0
  43. package/src/shared/utils/errors/serviceErrors.ts +36 -0
  44. package/src/shared/utils/logger.ts +140 -0
  45. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "3.1.9",
3
+ "version": "3.1.11",
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",
@@ -1,10 +1,10 @@
1
- import { useEffect, useState, useCallback } from "react";
2
- import { onSnapshot, type DocumentSnapshot } from "firebase/firestore";
3
- import type { UserCredits } from "../core/Credits";
1
+ import { useMemo } from "react";
2
+ import type { DocumentReference } from "firebase/firestore";
3
+ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
4
4
  import { getCreditsConfig } from "../infrastructure/CreditsRepositoryManager";
5
5
  import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
6
- import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore/collectionUtils";
7
- import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
6
+ import { requireFirestore, buildDocRef } from "../../../shared/infrastructure/firestore/collectionUtils";
7
+ import { useFirestoreDocumentRealTime } from "../../../shared/presentation/hooks/useFirestoreRealTime";
8
8
 
9
9
  /**
10
10
  * Real-time sync for credits using Firestore onSnapshot.
@@ -20,91 +20,49 @@ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
20
20
  * @returns Credits state and loading status
21
21
  */
22
22
  export function useCreditsRealTime(userId: string | null | undefined) {
23
- const [credits, setCredits] = useState<UserCredits | null>(null);
24
- const [isLoading, setIsLoading] = useState(true);
25
- const [error, setError] = useState<Error | null>(null);
26
-
27
- useEffect(() => {
28
- // Reset state when userId changes
29
- if (!userId) {
30
- setCredits(null);
31
- setIsLoading(false);
32
- setError(null);
33
- return;
34
- }
35
-
36
- setIsLoading(true);
37
- setError(null);
38
-
39
- try {
40
- const db = requireFirestore();
41
- const config = getCreditsConfig();
23
+ // Build document reference
24
+ const docRef = useMemo(() => {
25
+ if (!userId) return null;
42
26
 
43
- // Build doc ref using same logic as repository
44
- const collectionConfig: CollectionConfig = {
45
- collectionName: config.collectionName,
46
- useUserSubcollection: config.useUserSubcollection,
47
- };
48
- const docRef = buildDocRef(db, userId, "balance", collectionConfig);
49
-
50
- // Real-time listener
51
- const unsubscribe = onSnapshot(
52
- docRef,
53
- (snapshot: DocumentSnapshot) => {
54
- if (snapshot.exists()) {
55
- const entity = mapCreditsDocumentToEntity(snapshot.data() as UserCreditsDocumentRead);
56
- setCredits(entity);
57
- } else {
58
- setCredits(null);
59
- }
60
- setIsLoading(false);
61
- },
62
- (err: Error) => {
63
- console.error("[useCreditsRealTime] Snapshot error:", err);
64
- setError(err);
65
- setIsLoading(false);
66
- }
67
- );
68
-
69
- return () => {
70
- unsubscribe();
71
- };
72
- } catch (err) {
73
- const error = err instanceof Error ? err : new Error(String(err));
74
- console.error("[useCreditsRealTime] Setup error:", err);
75
- setError(error);
76
- setIsLoading(false);
77
- }
27
+ const db = requireFirestore();
28
+ const config = getCreditsConfig();
29
+ const ref = buildDocRef(db, userId, "balance", config);
30
+ return ref as DocumentReference<UserCreditsDocumentRead>;
78
31
  }, [userId]);
79
32
 
80
- const refetch = useCallback(() => {
81
- // Real-time sync doesn't need refetch, but keep for API compatibility
82
- // The snapshot listener will automatically update when data changes
83
- if (__DEV__) {
84
- console.warn("[useCreditsRealTime] Refetch called - not needed for real-time sync");
85
- }
86
- }, []);
33
+ // Use generic real-time sync hook
34
+ const { data, isLoading, error, refetch } = useFirestoreDocumentRealTime(
35
+ userId,
36
+ docRef,
37
+ mapCreditsDocumentToEntity,
38
+ "useCreditsRealTime"
39
+ );
87
40
 
88
41
  return {
89
- credits,
42
+ credits: data,
90
43
  isLoading,
91
44
  error,
92
- refetch, // No-op but kept for compatibility
45
+ refetch,
93
46
  };
94
47
  }
95
48
 
96
49
  /**
97
50
  * Hook to get derived credit values with real-time sync.
98
51
  * This is the real-time equivalent of the computed values in useCredits.
52
+ *
53
+ * PERFORMANCE: Uses useMemo to avoid recalculating on every render
99
54
  */
100
55
  export function useCreditsRealTimeDerived(userId: string | null | undefined) {
101
56
  const { credits, isLoading } = useCreditsRealTime(userId);
102
57
 
103
- const hasCredits = (credits?.credits ?? 0) > 0;
104
- const creditsPercent = credits ? Math.min(
105
- (credits.credits / credits.creditLimit) * 100,
106
- 100
107
- ) : 0;
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
108
66
 
109
67
  return {
110
68
  hasCredits,
@@ -1,27 +1,6 @@
1
- import { isValidNumber, isNonNegativeNumber } from "../../../shared/utils/validators";
1
+ /**
2
+ * Credit validation utilities
3
+ * Re-exports validation functions from creditCalculations for backwards compatibility
4
+ */
2
5
 
3
- const isValidBalance = (balance: number | null | undefined): balance is number => {
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
- * Handles purchase and restore operations with premium verification.
5
- * Ref management and success checking extracted to utilities.
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, useCallback, useRef, useMemo } from "react";
9
- import type { PurchasesPackage } from "react-native-purchases";
10
- import { usePremiumVerification } from "./usePaywallActions.utils";
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
- // PURCHASE HANDLER
64
- // ─────────────────────────────────────────────────────────────
65
-
66
- const handlePurchase = useCallback(async () => {
67
- const currentSelectedId = selectedPlanId;
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
- // RESET
170
- // ─────────────────────────────────────────────────────────────
57
+ const handleRestore = useRestoreHandler({
58
+ isProcessingRef,
59
+ callbacksRef,
60
+ });
171
61
 
172
- const resetState = useCallback(() => {
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, resetState]);
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
+ }