@umituz/react-native-subscription 2.33.9 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/DeductCreditsCommand.ts +11 -0
  3. package/src/domains/credits/application/creditOperationUtils.ts +5 -1
  4. package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +6 -1
  5. package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +11 -4
  6. package/src/domains/credits/presentation/useCredits.ts +2 -7
  7. package/src/domains/paywall/hooks/usePaywallActions.ts +50 -16
  8. package/src/domains/revenuecat/infrastructure/services/ConfigurationStateManager.ts +16 -4
  9. package/src/domains/revenuecat/infrastructure/services/RevenueCatInitializer.ts +17 -4
  10. package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +63 -4
  11. package/src/domains/subscription/application/SubscriptionAuthListener.ts +45 -8
  12. package/src/domains/subscription/application/SubscriptionSyncService.ts +24 -6
  13. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +32 -9
  14. package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +30 -3
  15. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +7 -4
  16. package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +3 -2
  17. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +9 -1
  18. package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +10 -0
  19. package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +6 -0
  20. package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +1 -4
  21. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +36 -11
  22. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +0 -1
  23. package/src/domains/subscription/infrastructure/managers/managerOperations.ts +10 -1
  24. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
  25. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +13 -5
  26. package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +17 -1
  27. package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +10 -1
  28. package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +23 -4
  29. package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +2 -7
  30. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +7 -3
  31. package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +2 -2
  32. package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +4 -4
  33. package/src/init/createSubscriptionInitModule.ts +1 -1
  34. package/src/shared/infrastructure/SubscriptionEventBus.ts +10 -5
  35. package/src/shared/presentation/hooks/useServiceCall.ts +15 -5
  36. package/src/domains/subscription/infrastructure/utils/UserIdProvider.ts +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.33.9",
3
+ "version": "2.35.0",
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",
@@ -13,6 +13,17 @@ export async function deductCreditsOperation(
13
13
  cost: number,
14
14
  userId: string
15
15
  ): Promise<DeductCreditsResult> {
16
+ if (!userId || userId.trim().length === 0) {
17
+ return {
18
+ success: false,
19
+ remainingCredits: null,
20
+ error: {
21
+ message: 'Valid userId is required for credit deduction',
22
+ code: 'INVALID_USER'
23
+ }
24
+ };
25
+ }
26
+
16
27
  try {
17
28
  const remaining = await runTransaction(async (tx: Transaction) => {
18
29
  const docSnap = await tx.get(creditsRef);
@@ -90,5 +90,9 @@ export function shouldSkipStatusSyncWrite(
90
90
  existingData.status === newCreditsData.status &&
91
91
  existingData.credits === newCreditsData.credits &&
92
92
  existingData.creditLimit === newCreditsData.creditLimit &&
93
- existingData.productId === newCreditsData.productId;
93
+ existingData.productId === newCreditsData.productId &&
94
+ existingData.willRenew === newCreditsData.willRenew &&
95
+ existingData.expirationDate === newCreditsData.expirationDate &&
96
+ existingData.canceledAt === newCreditsData.canceledAt &&
97
+ existingData.billingIssueDetectedAt === newCreditsData.billingIssueDetectedAt;
94
98
  }
@@ -72,7 +72,12 @@ export async function initializeCreditsWithRetry(params: InitializeCreditsParams
72
72
  lastError = error;
73
73
 
74
74
  if (isTransientError(error) && attempt < maxRetries - 1) {
75
- await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
75
+ const baseDelay = 100;
76
+ const maxDelay = 5000;
77
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
78
+ const jitter = Math.random() * baseDelay;
79
+ const delay = Math.min(exponentialDelay + jitter, maxDelay);
80
+ await new Promise(resolve => setTimeout(resolve, delay));
76
81
  continue;
77
82
  }
78
83
  break;
@@ -20,14 +20,21 @@ export const useDeductCredit = ({
20
20
  try {
21
21
  const res = await mutation.mutateAsync(cost);
22
22
  if (!res.success) {
23
- if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
23
+ if (res.error?.code === "CREDITS_EXHAUSTED") {
24
+ onCreditsExhausted?.();
25
+ }
24
26
  return false;
25
27
  }
26
28
  return true;
27
- } catch {
28
- return false;
29
+ } catch (error) {
30
+ console.error('[useDeductCredit] Unexpected error during credit deduction', {
31
+ cost,
32
+ userId,
33
+ error
34
+ });
35
+ throw error;
29
36
  }
30
- }, [mutation, onCreditsExhausted]);
37
+ }, [mutation, onCreditsExhausted, userId]);
31
38
 
32
39
  const deductCredits = useCallback(async (cost: number): Promise<boolean> => {
33
40
  return await deductCredit(cost);
@@ -52,23 +52,18 @@ export const useCredits = (): UseCreditsResult => {
52
52
  });
53
53
 
54
54
  const queryClient = useQueryClient();
55
- const queryClientRef = useRef(queryClient);
56
-
57
- useEffect(() => {
58
- queryClientRef.current = queryClient;
59
- }, [queryClient]);
60
55
 
61
56
  useEffect(() => {
62
57
  if (!isAuthenticated(userId)) return undefined;
63
58
 
64
59
  const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
65
60
  if (updatedUserId === userId) {
66
- queryClientRef.current.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
61
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
67
62
  }
68
63
  });
69
64
 
70
65
  return unsubscribe;
71
- }, [userId]);
66
+ }, [userId, queryClient]);
72
67
 
73
68
  const credits = data ?? null;
74
69
 
@@ -2,7 +2,7 @@
2
2
  * usePaywallActions Hook
3
3
  * Encapsulates purchase and restore flow for the paywall.
4
4
  */
5
- import { useState, useCallback } from "react";
5
+ import { useState, useCallback, useRef, useEffect } from "react";
6
6
  import type { PurchasesPackage } from "react-native-purchases";
7
7
  import { usePurchaseLoadingStore } from "../../subscription/presentation/stores";
8
8
  import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
@@ -35,53 +35,87 @@ export function usePaywallActions({
35
35
 
36
36
  const isProcessing = isLocalProcessing || isGlobalPurchasing;
37
37
 
38
+ const onPurchaseRef = useRef(onPurchase);
39
+ const onRestoreRef = useRef(onRestore);
40
+ const onPurchaseSuccessRef = useRef(onPurchaseSuccess);
41
+ const onPurchaseErrorRef = useRef(onPurchaseError);
42
+ const onAuthRequiredRef = useRef(onAuthRequired);
43
+ const onCloseRef = useRef(onClose);
44
+
45
+ useEffect(() => {
46
+ onPurchaseRef.current = onPurchase;
47
+ onRestoreRef.current = onRestore;
48
+ onPurchaseSuccessRef.current = onPurchaseSuccess;
49
+ onPurchaseErrorRef.current = onPurchaseError;
50
+ onAuthRequiredRef.current = onAuthRequired;
51
+ onCloseRef.current = onClose;
52
+ });
53
+
38
54
  const handlePurchase = useCallback(async () => {
39
- // If no plan selected, use the first available one as fallback or return
55
+ console.log('🔵 [usePaywallActions] handlePurchase called', {
56
+ selectedPlanId,
57
+ packagesCount: packages.length,
58
+ isProcessing,
59
+ hasOnPurchase: !!onPurchaseRef.current
60
+ });
61
+
40
62
  const planId = selectedPlanId || (packages.length > 0 ? packages[0]?.product.identifier : null);
41
-
42
- if (!planId || !onPurchase || isProcessing) {
43
- if (!planId && onAuthRequired) onAuthRequired();
63
+
64
+ if (!planId || !onPurchaseRef.current || isProcessing) {
65
+ console.log('⚠️ [usePaywallActions] Purchase blocked', {
66
+ noPlanId: !planId,
67
+ noCallback: !onPurchaseRef.current,
68
+ isProcessing
69
+ });
70
+ if (!planId && onAuthRequiredRef.current) onAuthRequiredRef.current();
44
71
  return;
45
72
  }
46
73
 
74
+ console.log('🟢 [usePaywallActions] Starting purchase', { planId });
47
75
  setIsLocalProcessing(true);
48
- // Map PurchaseSource to store's expected "manual" | "auto-execution"
49
76
  startPurchase(planId, "manual");
50
77
 
51
78
  try {
52
79
  const pkg = packages.find((p) => p.product.identifier === planId);
80
+ console.log('📦 [usePaywallActions] Package found:', !!pkg);
81
+
53
82
  if (pkg) {
54
- const success = await onPurchase(pkg);
83
+ console.log('🚀 [usePaywallActions] Calling onPurchase callback');
84
+ const success = await onPurchaseRef.current(pkg);
85
+ console.log('✅ [usePaywallActions] onPurchase completed', { success });
86
+
55
87
  if (success !== false) {
56
- onPurchaseSuccess?.();
57
- onClose?.(); // Close on success if provided
88
+ onPurchaseSuccessRef.current?.();
89
+ onCloseRef.current?.();
58
90
  }
59
91
  }
60
92
  } catch (error) {
93
+ console.error('❌ [usePaywallActions] Purchase error:', error);
61
94
  const err = error instanceof Error ? error : new Error(String(error));
62
- onPurchaseError?.(err);
95
+ onPurchaseErrorRef.current?.(err);
63
96
  } finally {
97
+ console.log('🏁 [usePaywallActions] Purchase flow finished');
64
98
  setIsLocalProcessing(false);
65
99
  endPurchase(planId);
66
100
  }
67
- }, [selectedPlanId, packages, onPurchase, isProcessing, startPurchase, endPurchase, onPurchaseSuccess, onPurchaseError, onAuthRequired, onClose]);
101
+ }, [selectedPlanId, packages, isProcessing, startPurchase, endPurchase]);
68
102
 
69
103
  const handleRestore = useCallback(async () => {
70
- if (!onRestore || isProcessing) return;
104
+ if (!onRestoreRef.current || isProcessing) return;
71
105
 
72
106
  setIsLocalProcessing(true);
73
107
  try {
74
- const success = await onRestore();
108
+ const success = await onRestoreRef.current();
75
109
  if (success !== false) {
76
- onPurchaseSuccess?.();
110
+ onPurchaseSuccessRef.current?.();
77
111
  }
78
112
  } catch (error) {
79
113
  const err = error instanceof Error ? error : new Error(String(error));
80
- onPurchaseError?.(err);
114
+ onPurchaseErrorRef.current?.(err);
81
115
  } finally {
82
116
  setIsLocalProcessing(false);
83
117
  }
84
- }, [onRestore, isProcessing, onPurchaseSuccess, onPurchaseError]);
118
+ }, [isProcessing]);
85
119
 
86
120
  const resetState = useCallback(() => {
87
121
  setSelectedPlanId(null);
@@ -22,21 +22,33 @@ export class ConfigurationStateManager {
22
22
  throw new Error('Configuration already in progress');
23
23
  }
24
24
 
25
+ let capturedResolve: ((value: InitializeResult) => void) | null = null;
26
+
25
27
  this._configurationPromise = new Promise((resolve) => {
28
+ capturedResolve = resolve;
26
29
  this._resolveConfiguration = resolve;
27
30
  });
28
31
 
29
32
  return (value: InitializeResult) => {
30
- if (this._resolveConfiguration) {
31
- this._resolveConfiguration(value);
33
+ if (capturedResolve) {
34
+ capturedResolve(value);
35
+ capturedResolve = null;
32
36
  }
33
37
  };
34
38
  }
35
39
 
36
40
  completeConfiguration(success: boolean): void {
37
41
  this._isPurchasesConfigured = success;
38
- this._configurationPromise = null;
39
- this._resolveConfiguration = null;
42
+
43
+ if (success) {
44
+ this._configurationPromise = null;
45
+ this._resolveConfiguration = null;
46
+ } else {
47
+ setTimeout(() => {
48
+ this._configurationPromise = null;
49
+ this._resolveConfiguration = null;
50
+ }, 1000);
51
+ }
40
52
  }
41
53
 
42
54
  reset(): void {
@@ -7,10 +7,13 @@ import { handleUserSwitch, handleInitialConfiguration, fetchCurrentUserData } fr
7
7
 
8
8
  export type { InitializerDeps } from "./RevenueCatInitializer.types";
9
9
 
10
+ const MAX_CONFIG_START_RETRIES = 3;
11
+
10
12
  export async function initializeSDK(
11
13
  deps: InitializerDeps,
12
14
  userId: string,
13
- apiKey?: string
15
+ apiKey?: string,
16
+ configStartRetryCount: number = 0
14
17
  ): Promise<InitializeResult> {
15
18
  if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
16
19
  return fetchCurrentUserData(deps);
@@ -36,7 +39,7 @@ export async function initializeSDK(
36
39
  return FAILED_INITIALIZATION_RESULT;
37
40
  }
38
41
 
39
- return initializeSDK(deps, userId, apiKey);
42
+ return initializeSDK(deps, userId, apiKey, configStartRetryCount);
40
43
  }
41
44
 
42
45
  const key = apiKey || resolveApiKey(deps.config);
@@ -48,12 +51,22 @@ export async function initializeSDK(
48
51
  try {
49
52
  resolveConfig = configState.startConfiguration();
50
53
  } catch (error) {
51
- console.error('[RevenueCatInitializer] Failed to start configuration', {
54
+ if (configStartRetryCount >= MAX_CONFIG_START_RETRIES) {
55
+ console.error('[RevenueCatInitializer] Max configuration start retries reached', {
56
+ userId,
57
+ retryCount: configStartRetryCount,
58
+ error
59
+ });
60
+ return FAILED_INITIALIZATION_RESULT;
61
+ }
62
+
63
+ console.error('[RevenueCatInitializer] Failed to start configuration, retrying', {
52
64
  userId,
65
+ retryCount: configStartRetryCount,
53
66
  error
54
67
  });
55
68
  await new Promise(resolve => setTimeout(resolve, CONFIGURATION_RETRY_DELAY_MS));
56
- return initializeSDK(deps, userId, apiKey);
69
+ return initializeSDK(deps, userId, apiKey, configStartRetryCount + 1);
57
70
  }
58
71
 
59
72
  const result = await handleInitialConfiguration(deps, userId, key);
@@ -3,6 +3,8 @@ import type { InitializeResult } from "../../../../shared/application/ports/IRev
3
3
  import type { InitializerDeps } from "./RevenueCatInitializer.types";
4
4
  import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
5
5
 
6
+ declare const __DEV__: boolean;
7
+
6
8
  function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: any): InitializeResult {
7
9
  const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
8
10
  return { success: true, offering: offerings.current, isPremium };
@@ -13,7 +15,7 @@ function normalizeUserId(userId: string): string | null {
13
15
  }
14
16
 
15
17
  function isAnonymousId(userId: string): boolean {
16
- return userId.startsWith('$RCAnonymous');
18
+ return userId.startsWith('$RCAnonymous') || userId.startsWith('device_');
17
19
  }
18
20
 
19
21
  export async function handleUserSwitch(
@@ -22,30 +24,64 @@ export async function handleUserSwitch(
22
24
  ): Promise<InitializeResult> {
23
25
  try {
24
26
  const currentAppUserId = await Purchases.getAppUserID();
25
- let customerInfo;
26
-
27
27
  const normalizedUserId = normalizeUserId(userId);
28
28
  const normalizedCurrentUserId = isAnonymousId(currentAppUserId) ? null : currentAppUserId;
29
29
 
30
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
31
+ console.log('[UserSwitchHandler] handleUserSwitch:', {
32
+ providedUserId: userId,
33
+ normalizedUserId: normalizedUserId || '(null - anonymous)',
34
+ currentAppUserId,
35
+ normalizedCurrentUserId: normalizedCurrentUserId || '(null - anonymous)',
36
+ needsSwitch: normalizedCurrentUserId !== normalizedUserId,
37
+ });
38
+ }
39
+
40
+ let customerInfo;
41
+
30
42
  if (normalizedCurrentUserId !== normalizedUserId) {
31
43
  if (normalizedUserId) {
44
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
45
+ console.log('[UserSwitchHandler] Calling Purchases.logIn() to switch from anonymous to:', normalizedUserId);
46
+ }
32
47
  const result = await Purchases.logIn(normalizedUserId);
33
48
  customerInfo = result.customerInfo;
49
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
50
+ console.log('[UserSwitchHandler] ✅ Purchases.logIn() successful, created:', result.created);
51
+ }
34
52
  } else {
53
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
54
+ console.log('[UserSwitchHandler] User is anonymous, fetching customer info');
55
+ }
35
56
  customerInfo = await Purchases.getCustomerInfo();
36
57
  }
37
58
  } else {
59
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
60
+ console.log('[UserSwitchHandler] No user switch needed, fetching current customer info');
61
+ }
38
62
  customerInfo = await Purchases.getCustomerInfo();
39
63
  }
40
64
 
41
65
  deps.setInitialized(true);
42
66
  deps.setCurrentUserId(normalizedUserId);
43
67
  const offerings = await Purchases.getOfferings();
68
+
69
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
70
+ console.log('[UserSwitchHandler] ✅ User switch completed successfully');
71
+ }
72
+
44
73
  return buildSuccessResult(deps, customerInfo, offerings);
45
74
  } catch (error) {
75
+ let currentAppUserId = 'unknown';
76
+ try {
77
+ currentAppUserId = await Purchases.getAppUserID();
78
+ } catch {
79
+ // Ignore error in error handler
80
+ }
81
+
46
82
  console.error('[UserSwitchHandler] Failed during user switch or fetch', {
47
83
  userId,
48
- currentAppUserId: await Purchases.getAppUserID().catch(() => 'unknown'),
84
+ currentAppUserId,
49
85
  error
50
86
  });
51
87
  return FAILED_INITIALIZATION_RESULT;
@@ -59,15 +95,38 @@ export async function handleInitialConfiguration(
59
95
  ): Promise<InitializeResult> {
60
96
  try {
61
97
  const normalizedUserId = normalizeUserId(userId);
98
+
99
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
100
+ console.log('[UserSwitchHandler] handleInitialConfiguration:', {
101
+ providedUserId: userId,
102
+ normalizedUserId: normalizedUserId || '(null - anonymous)',
103
+ apiKeyPrefix: apiKey.substring(0, 5) + '...',
104
+ isTestKey: apiKey.startsWith('test_'),
105
+ });
106
+ }
107
+
62
108
  await Purchases.configure({ apiKey, appUserID: normalizedUserId });
63
109
  deps.setInitialized(true);
64
110
  deps.setCurrentUserId(normalizedUserId);
65
111
 
112
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
113
+ console.log('[UserSwitchHandler] ✅ Purchases.configure() successful');
114
+ }
115
+
66
116
  const [customerInfo, offerings] = await Promise.all([
67
117
  Purchases.getCustomerInfo(),
68
118
  Purchases.getOfferings(),
69
119
  ]);
70
120
 
121
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
122
+ const currentUserId = await Purchases.getAppUserID();
123
+ console.log('[UserSwitchHandler] ✅ Initial configuration completed:', {
124
+ revenueCatUserId: currentUserId,
125
+ activeEntitlements: Object.keys(customerInfo.entitlements.active),
126
+ offeringsCount: offerings.all ? Object.keys(offerings.all).length : 0,
127
+ });
128
+ }
129
+
71
130
  return buildSuccessResult(deps, customerInfo, offerings);
72
131
  } catch (error) {
73
132
  console.error('[UserSwitchHandler] SDK configuration failed', {
@@ -1,18 +1,38 @@
1
1
  import type { FirebaseAuthLike } from "./SubscriptionInitializerTypes";
2
2
 
3
+ declare const __DEV__: boolean;
4
+
3
5
  /**
4
6
  * Gets the current user ID from Firebase auth.
5
- * Returns undefined for anonymous users to prevent RevenueCat from using anonymous Firebase UIDs.
7
+ * Returns undefined for anonymous users to let RevenueCat generate its own anonymous ID.
6
8
  */
7
9
  export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
8
10
  const auth = getAuth();
9
- if (!auth) return undefined;
11
+ if (!auth) {
12
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
13
+ console.log('[SubscriptionAuthListener] No auth available');
14
+ }
15
+ return undefined;
16
+ }
10
17
 
11
18
  const user = auth.currentUser;
12
- if (!user) return undefined;
19
+ if (!user) {
20
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
+ console.log('[SubscriptionAuthListener] No current user');
22
+ }
23
+ return undefined;
24
+ }
25
+
26
+ if (user.isAnonymous) {
27
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
28
+ console.log('[SubscriptionAuthListener] Anonymous user - returning undefined (RevenueCat will use its own ID)');
29
+ }
30
+ return undefined;
31
+ }
13
32
 
14
- // Don't return userId for anonymous users - let RevenueCat use its own anonymous ID
15
- if (user.isAnonymous) return undefined;
33
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
34
+ console.log('[SubscriptionAuthListener] Authenticated user:', user.uid);
35
+ }
16
36
 
17
37
  return user.uid;
18
38
  };
@@ -20,18 +40,35 @@ export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string
20
40
  /**
21
41
  * Sets up auth state listener that will re-initialize subscription
22
42
  * when user auth state changes (login/logout).
23
- * Returns undefined for anonymous users to prevent RevenueCat from using anonymous Firebase UIDs.
43
+ * Returns undefined for anonymous users to let RevenueCat generate its own anonymous ID.
24
44
  */
25
45
  export const setupAuthStateListener = (
26
46
  getAuth: () => FirebaseAuthLike | null,
27
47
  onUserChange: (userId: string | undefined) => void
28
48
  ): (() => void) | null => {
29
49
  const auth = getAuth();
30
- if (!auth) return null;
50
+ if (!auth) {
51
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
52
+ console.log('[SubscriptionAuthListener] Cannot setup listener - no auth available');
53
+ }
54
+ return null;
55
+ }
56
+
57
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
58
+ console.log('[SubscriptionAuthListener] Setting up auth state listener');
59
+ }
31
60
 
32
61
  return auth.onAuthStateChanged((user) => {
33
- // Don't pass userId for anonymous users - let RevenueCat use its own anonymous ID
34
62
  const userId = (user && !user.isAnonymous) ? user.uid : undefined;
63
+
64
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
65
+ console.log('[SubscriptionAuthListener] 🔔 Auth state changed:', {
66
+ hasUser: !!user,
67
+ isAnonymous: user?.isAnonymous,
68
+ userId: userId || '(undefined - anonymous)',
69
+ });
70
+ }
71
+
35
72
  onUserChange(userId);
36
73
  });
37
74
  };
@@ -14,8 +14,13 @@ export class SubscriptionSyncService {
14
14
  try {
15
15
  await this.processor.processPurchase(userId, productId, customerInfo, source);
16
16
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
17
- } catch (_err) {
18
- // Swallow error - event bus consumers handle failures
17
+ } catch (error) {
18
+ console.error('[SubscriptionSyncService] Purchase processing failed', {
19
+ userId,
20
+ productId,
21
+ source,
22
+ error
23
+ });
19
24
  }
20
25
  }
21
26
 
@@ -23,8 +28,13 @@ export class SubscriptionSyncService {
23
28
  try {
24
29
  await this.processor.processRenewal(userId, productId, newExpirationDate, customerInfo);
25
30
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
26
- } catch (_err) {
27
- // Swallow error - event bus consumers handle failures
31
+ } catch (error) {
32
+ console.error('[SubscriptionSyncService] Renewal processing failed', {
33
+ userId,
34
+ productId,
35
+ newExpirationDate,
36
+ error
37
+ });
28
38
  }
29
39
  }
30
40
 
@@ -39,8 +49,16 @@ export class SubscriptionSyncService {
39
49
  try {
40
50
  await this.processor.processStatusChange(userId, isPremium, productId, expiresAt, willRenew, periodType);
41
51
  subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
42
- } catch (_err) {
43
- // Swallow error - event bus consumers handle failures
52
+ } catch (error) {
53
+ console.error('[SubscriptionSyncService] Status change processing failed', {
54
+ userId,
55
+ isPremium,
56
+ productId,
57
+ expiresAt,
58
+ willRenew,
59
+ periodType,
60
+ error
61
+ });
44
62
  }
45
63
  }
46
64
  }
@@ -10,18 +10,41 @@ function validatePeriodType(periodType: string | undefined): PeriodType | null {
10
10
  }
11
11
 
12
12
  export const extractRevenueCatData = (customerInfo: CustomerInfo, entitlementId: string): RevenueCatData => {
13
+ if (!customerInfo) {
14
+ throw new Error('[extractRevenueCatData] customerInfo is required');
15
+ }
16
+ if (!entitlementId) {
17
+ throw new Error('[extractRevenueCatData] entitlementId is required');
18
+ }
19
+
13
20
  const entitlement = customerInfo.entitlements.active[entitlementId]
14
21
  ?? customerInfo.entitlements.all[entitlementId];
15
22
 
23
+ const isPremium = !!customerInfo.entitlements.active[entitlementId];
24
+
25
+ if (!entitlement) {
26
+ return {
27
+ expirationDate: null,
28
+ willRenew: null,
29
+ originalTransactionId: null,
30
+ periodType: null,
31
+ isPremium: false,
32
+ unsubscribeDetectedAt: null,
33
+ billingIssueDetectedAt: null,
34
+ store: null,
35
+ ownershipType: null,
36
+ };
37
+ }
38
+
16
39
  return {
17
- expirationDate: entitlement?.expirationDate ?? customerInfo.latestExpirationDate ?? null,
18
- willRenew: entitlement?.willRenew ?? false,
19
- originalTransactionId: customerInfo.originalAppUserId ?? null,
20
- periodType: validatePeriodType(entitlement?.periodType),
21
- isPremium: !!customerInfo.entitlements.active[entitlementId],
22
- unsubscribeDetectedAt: entitlement?.unsubscribeDetectedAt ?? null,
23
- billingIssueDetectedAt: entitlement?.billingIssueDetectedAt ?? null,
24
- store: entitlement?.store ?? null,
25
- ownershipType: entitlement?.ownershipType ?? null,
40
+ expirationDate: entitlement.expirationDate ?? null,
41
+ willRenew: entitlement.willRenew ?? null,
42
+ originalTransactionId: entitlement.originalPurchaseDate ?? null,
43
+ periodType: validatePeriodType(entitlement.periodType),
44
+ isPremium,
45
+ unsubscribeDetectedAt: entitlement.unsubscribeDetectedAt ?? null,
46
+ billingIssueDetectedAt: entitlement.billingIssueDetectedAt ?? null,
47
+ store: entitlement.store ?? null,
48
+ ownershipType: entitlement.ownershipType ?? null,
26
49
  };
27
50
  };
@@ -2,8 +2,13 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
2
2
  import { getCurrentUserId, setupAuthStateListener } from "../SubscriptionAuthListener";
3
3
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
4
 
5
- export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<void> {
5
+ declare const __DEV__: boolean;
6
+
7
+ export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
6
8
  const initializeInBackground = async (userId?: string): Promise<void> => {
9
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
10
+ console.log('[BackgroundInitializer] initializeInBackground called with userId:', userId || '(undefined - anonymous)');
11
+ }
7
12
  await SubscriptionManager.initialize(userId);
8
13
  };
9
14
 
@@ -12,10 +17,32 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
12
17
  throw new Error("Firebase auth is not available");
13
18
  }
14
19
 
20
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
+ console.log('[BackgroundInitializer] Starting background initialization');
22
+ }
23
+
15
24
  const initialUserId = getCurrentUserId(() => auth);
25
+
26
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
27
+ console.log('[BackgroundInitializer] Initial userId:', initialUserId || '(undefined - anonymous)');
28
+ }
29
+
16
30
  await initializeInBackground(initialUserId);
17
31
 
18
- setupAuthStateListener(() => auth, (newUserId) => {
19
- initializeInBackground(newUserId);
32
+ const unsubscribe = setupAuthStateListener(() => auth, async (newUserId) => {
33
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
34
+ console.log('[BackgroundInitializer] Auth state listener triggered, reinitializing with userId:', newUserId || '(undefined - anonymous)');
35
+ }
36
+ try {
37
+ await initializeInBackground(newUserId);
38
+ } catch (error) {
39
+ console.error('[BackgroundInitializer] Failed to reinitialize on auth change', { userId: newUserId, error });
40
+ }
20
41
  });
42
+
43
+ return () => {
44
+ if (unsubscribe) {
45
+ unsubscribe();
46
+ }
47
+ };
21
48
  }