@umituz/react-native-subscription 2.27.95 → 2.27.97

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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/core/Credits.ts +3 -11
  3. package/src/domains/credits/infrastructure/CreditsRepository.ts +28 -1
  4. package/src/domains/paywall/components/PaywallContainer.tsx +17 -1
  5. package/src/domains/paywall/components/PaywallContainer.types.ts +2 -1
  6. package/src/domains/paywall/hooks/usePaywallActions.ts +1 -1
  7. package/src/domains/subscription/application/SubscriptionInitializer.ts +1 -1
  8. package/src/domains/subscription/application/SubscriptionSyncService.ts +32 -5
  9. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
  10. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +32 -12
  11. package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +4 -4
  12. package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +1 -7
  13. package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -2
  14. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +5 -4
  15. package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +20 -7
  16. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -1
  17. package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +55 -16
  18. package/src/domains/subscription/presentation/screens/components/CreditsList.tsx +14 -1
  19. package/src/domains/subscription/presentation/screens/components/DevTestSection.tsx +10 -2
  20. package/src/domains/subscription/presentation/screens/components/SubscriptionActions.tsx +6 -1
  21. package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +20 -1
  22. package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +13 -1
  23. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +1 -1
  24. package/src/domains/subscription/presentation/useFeatureGate.ts +11 -7
  25. package/src/domains/subscription/presentation/usePaywallVisibility.ts +1 -1
  26. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -5
  27. package/src/init/index.ts +0 -3
  28. package/src/presentation/hooks/index.ts +0 -4
  29. package/src/shared/infrastructure/SubscriptionEventBus.ts +27 -0
  30. package/src/types/i18next.d.ts +2 -0
  31. package/src/utils/packageTypeDetector.ts +0 -4
  32. package/src/domains/subscription/presentation/types/README.md +0 -22
  33. package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +0 -153
  34. package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +0 -74
  35. package/src/domains/subscription/presentation/useAuthSubscriptionSync.ts +0 -63
  36. package/src/domains/subscription/presentation/usePremiumGate.ts +0 -84
  37. package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +0 -148
  38. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +0 -115
  39. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +0 -57
@@ -135,9 +135,9 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
135
135
  if (typeof __DEV__ !== "undefined" && __DEV__) {
136
136
  console.log("[useFeatureGate] requireFeature", {
137
137
  isAuthenticated,
138
- hasSubscription,
138
+ hasSubscription: hasSubscriptionRef.current,
139
139
  creditBalance: creditBalanceRef.current,
140
- requiredCredits,
140
+ requiredCredits: requiredCreditsRef.current,
141
141
  isCreditsLoaded,
142
142
  });
143
143
  }
@@ -155,25 +155,29 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
155
155
  return;
156
156
  }
157
157
 
158
- if (hasSubscription) {
158
+ // Use ref values to avoid stale closure
159
+ const currentHasSubscription = hasSubscriptionRef.current;
160
+ const currentBalance = creditBalanceRef.current;
161
+ const currentRequiredCredits = requiredCreditsRef.current;
162
+
163
+ if (currentHasSubscription) {
159
164
  action();
160
165
  return;
161
166
  }
162
167
 
163
- const currentBalance = creditBalanceRef.current;
164
- if (currentBalance < requiredCredits) {
168
+ if (currentBalance < currentRequiredCredits) {
165
169
  if (typeof __DEV__ !== "undefined" && __DEV__) {
166
170
  console.log("[useFeatureGate] No credits, showing paywall");
167
171
  }
168
172
  pendingActionRef.current = action;
169
173
  isWaitingForPurchaseRef.current = true;
170
- onShowPaywall(requiredCredits);
174
+ onShowPaywallRef.current(currentRequiredCredits);
171
175
  return;
172
176
  }
173
177
 
174
178
  action();
175
179
  },
176
- [isAuthenticated, hasSubscription, requiredCredits, onShowAuthModal, onShowPaywall, isCreditsLoaded]
180
+ [isAuthenticated, onShowAuthModal, isCreditsLoaded]
177
181
  );
178
182
 
179
183
  const hasCredits = creditBalance >= requiredCredits;
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { useCallback, useSyncExternalStore } from "react";
8
- import type { PurchaseSource } from "../../credits/core/Credits";
8
+ import type { PurchaseSource } from "../core/SubscriptionConstants";
9
9
 
10
10
  type Listener = () => void;
11
11
 
@@ -48,11 +48,7 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
48
48
  }
49
49
  },
50
50
  enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
51
- staleTime: 30 * 1000, // 30 seconds
52
- gcTime: 5 * 60 * 1000, // 5 minutes
53
- refetchOnMount: "always",
54
- refetchOnWindowFocus: true,
55
- refetchOnReconnect: true,
51
+
56
52
  });
57
53
 
58
54
  return {
package/src/init/index.ts CHANGED
@@ -7,6 +7,3 @@ export {
7
7
  createSubscriptionInitModule,
8
8
  type SubscriptionInitModuleConfig,
9
9
  } from './createSubscriptionInitModule';
10
-
11
- // Re-export InitModule from design-system for convenience
12
- export type { InitModule } from '@umituz/react-native-design-system';
@@ -1,13 +1,9 @@
1
1
  export * from "../../domains/subscription/presentation/useAuthAwarePurchase";
2
- export * from "../../domains/subscription/presentation/useAuthSubscriptionSync";
3
- export * from "../../domains/subscription/presentation/useSavedPurchaseAutoExecution";
4
2
  export * from "../../domains/credits/presentation/useCredits";
5
3
  export * from "../../domains/credits/presentation/useDeductCredit";
6
4
  export * from "../../domains/subscription/presentation/useFeatureGate";
7
5
  export * from "../../domains/subscription/presentation/usePaywallVisibility";
8
6
  export * from "../../domains/subscription/presentation/usePremium";
9
- export * from "../../domains/subscription/presentation/usePremiumGate";
10
- export * from "../../domains/subscription/presentation/useSubscriptionSettingsConfig";
11
7
  export * from "../../domains/subscription/presentation/useSubscriptionStatus";
12
8
  export * from "./feedback/usePaywallFeedback";
13
9
  export * from "./feedback/useFeedbackSubmit";
@@ -28,6 +28,11 @@ export class SubscriptionEventBus {
28
28
  const listeners = this.listeners[event];
29
29
  if (listeners) {
30
30
  this.listeners[event] = listeners.filter(l => l !== callback);
31
+
32
+ // Clean up empty event arrays to prevent memory leak
33
+ if (this.listeners[event].length === 0) {
34
+ delete this.listeners[event];
35
+ }
31
36
  }
32
37
  };
33
38
  }
@@ -42,6 +47,28 @@ export class SubscriptionEventBus {
42
47
  }
43
48
  });
44
49
  }
50
+
51
+ /**
52
+ * Clear all listeners for a specific event or all events
53
+ * Useful for cleanup during testing or app state reset
54
+ */
55
+ clear(event?: string): void {
56
+ if (event) {
57
+ delete this.listeners[event];
58
+ } else {
59
+ this.listeners = {};
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get listener count for debugging
65
+ */
66
+ getListenerCount(event?: string): number {
67
+ if (event) {
68
+ return this.listeners[event]?.length ?? 0;
69
+ }
70
+ return Object.values(this.listeners).reduce((total, arr) => total + arr.length, 0);
71
+ }
45
72
  }
46
73
 
47
74
  export const subscriptionEventBus = SubscriptionEventBus.getInstance();
@@ -0,0 +1,2 @@
1
+ declare module 'i18next';
2
+ declare module 'react-i18next';
@@ -7,10 +7,6 @@ import { PACKAGE_TYPE, type PackageType } from "../domains/subscription/core/Sub
7
7
 
8
8
  export type SubscriptionPackageType = PackageType;
9
9
 
10
- /**
11
- * Check if identifier is a credit package (consumable purchase)
12
- * Credit packages use a different system and don't need type detection
13
- */
14
10
  /**
15
11
  * Check if identifier is a credit package (consumable purchase)
16
12
  * Credit packages use a different system and don't need type detection
@@ -1,22 +0,0 @@
1
- # Presentation Types
2
-
3
- TypeScript type definitions and interfaces for the presentation layer.
4
-
5
- ## Overview
6
-
7
- This directory contains all type definitions used by presentation components and hooks.
8
-
9
- ## Contents
10
-
11
- ### Subscription Types
12
-
13
- - **SubscriptionSettingsTypes.ts** - Configuration types for subscription settings UI
14
- - **PaywallTypes.ts** - Paywall component types
15
- - **SubscriptionTypes.ts** - General subscription UI types
16
-
17
- ## Key Exports
18
-
19
- ## Related
20
-
21
- - [Hooks](../hooks/README.md)
22
- - [Components](../components/README.md)
@@ -1,153 +0,0 @@
1
- /**
2
- * Subscription Detail Types
3
- * Type definitions for subscription detail screen and components
4
- */
5
-
6
- import type { SubscriptionStatusType } from "../../core/SubscriptionStatus";
7
- import type { CreditInfo } from "../components/details/PremiumDetailsCardTypes";
8
-
9
- export type { SubscriptionStatusType, CreditInfo };
10
-
11
- /** Translation strings for subscription detail screen */
12
- export interface SubscriptionDetailTranslations {
13
- title: string;
14
- statusLabel: string;
15
- statusActive: string;
16
- statusExpired: string;
17
- statusInactive: string;
18
- statusCanceled: string;
19
- /** Free status label */
20
- statusFree: string;
21
- /** Trial status label (defaults to statusActive if not provided) */
22
- statusTrial?: string;
23
- /** Trial canceled status label (defaults to statusCanceled if not provided) */
24
- statusTrialCanceled?: string;
25
- expiresLabel: string;
26
- purchasedLabel: string;
27
- lifetimeLabel: string;
28
- creditsTitle: string;
29
- remainingLabel: string;
30
- usageTitle?: string;
31
- manageButton: string;
32
- upgradeButton: string;
33
- creditsResetInfo?: string;
34
- }
35
-
36
- /** Dev test action callbacks */
37
- export interface DevTestActions {
38
- onTestRenewal: () => Promise<void>;
39
- onCheckCredits: () => void;
40
- onTestDuplicate: () => Promise<void>;
41
- }
42
-
43
- /** Dev tools configuration */
44
- export interface DevToolsConfig {
45
- actions: DevTestActions;
46
- title?: string;
47
- }
48
-
49
- /** Benefit item for upgrade prompt */
50
- export interface UpgradeBenefit {
51
- icon?: string;
52
- text: string;
53
- }
54
-
55
- /** Upgrade prompt configuration */
56
- export interface UpgradePromptConfig {
57
- title: string;
58
- subtitle?: string;
59
- benefits?: UpgradeBenefit[];
60
- }
61
-
62
- /** Display flags - centralized UI visibility control */
63
- export interface SubscriptionDisplayFlags {
64
- showHeader: boolean;
65
- showCredits: boolean;
66
- showUpgradePrompt: boolean;
67
- showExpirationDate: boolean;
68
- }
69
-
70
- /** Configuration for subscription detail screen */
71
- export interface SubscriptionDetailConfig {
72
- statusType: SubscriptionStatusType;
73
- isPremium: boolean;
74
- display: SubscriptionDisplayFlags;
75
- expirationDate?: string | null;
76
- purchaseDate?: string | null;
77
- isLifetime?: boolean;
78
- daysRemaining?: number | null;
79
- willRenew?: boolean;
80
- credits?: CreditInfo[];
81
- translations: SubscriptionDetailTranslations;
82
- onManageSubscription?: () => void;
83
- onUpgrade?: () => void;
84
- devTools?: DevToolsConfig;
85
- upgradePrompt?: UpgradePromptConfig;
86
- }
87
-
88
- /** Props for subscription detail screen */
89
- export interface SubscriptionDetailScreenProps {
90
- config: SubscriptionDetailConfig;
91
- }
92
-
93
- /** Props for subscription header component */
94
- export interface SubscriptionHeaderProps {
95
- statusType: SubscriptionStatusType;
96
- showExpirationDate: boolean;
97
- isLifetime?: boolean;
98
- expirationDate?: string | null;
99
- purchaseDate?: string | null;
100
- daysRemaining?: number | null;
101
- translations: Pick<
102
- SubscriptionDetailTranslations,
103
- | "title"
104
- | "statusLabel"
105
- | "statusActive"
106
- | "statusExpired"
107
- | "statusInactive"
108
- | "statusCanceled"
109
- | "statusTrial"
110
- | "statusTrialCanceled"
111
- | "expiresLabel"
112
- | "purchasedLabel"
113
- | "lifetimeLabel"
114
- > & { statusFree: string };
115
- }
116
-
117
- /** Props for credits list component */
118
- export interface CreditsListProps {
119
- credits: CreditInfo[];
120
- title?: string;
121
- description?: string;
122
- remainingLabel?: string;
123
- }
124
-
125
- /** Props for credit row component */
126
- export interface CreditRowProps {
127
- label: string;
128
- current: number;
129
- total: number;
130
- remainingLabel?: string;
131
- }
132
-
133
- /** Props for subscription actions component */
134
- export interface SubscriptionActionsProps {
135
- isPremium: boolean;
136
- upgradeButtonLabel?: string;
137
- onUpgrade?: () => void;
138
- }
139
-
140
- /** Props for dev test section */
141
- export interface DevTestSectionProps {
142
- actions: DevTestActions;
143
- title?: string;
144
- }
145
-
146
- /** Props for upgrade prompt component */
147
- export interface UpgradePromptProps {
148
- title: string;
149
- subtitle?: string;
150
- benefits?: UpgradeBenefit[];
151
- upgradeButtonLabel?: string;
152
- onUpgrade?: () => void;
153
- }
@@ -1,74 +0,0 @@
1
- /**
2
- * Subscription Settings Types
3
- * Type definitions for subscription settings configuration
4
- */
5
-
6
- import type { SubscriptionStatusType } from "../../core/SubscriptionConstants";
7
- import type {
8
- SubscriptionDetailConfig,
9
- UpgradePromptConfig,
10
- } from "./SubscriptionDetailTypes";
11
-
12
- export type { SubscriptionStatusType, UpgradePromptConfig };
13
-
14
- /** Configuration for settings list item */
15
- export interface SubscriptionSettingsItemConfig {
16
- title: string;
17
- description?: string;
18
- isPremium: boolean;
19
- statusLabel: string;
20
- icon?: string;
21
- onPress?: () => void;
22
- }
23
-
24
- /** Complete subscription settings configuration */
25
- export interface SubscriptionSettingsConfig {
26
- /** Whether subscription section should be shown */
27
- enabled: boolean;
28
- /** Config for settings list item */
29
- settingsItem: SubscriptionSettingsItemConfig;
30
- /** Config for detail screen */
31
- sectionConfig: SubscriptionDetailConfig;
32
- }
33
-
34
- /** Translation strings for subscription settings */
35
- export interface SubscriptionSettingsTranslations {
36
- /** Settings item title */
37
- title: string;
38
- /** Settings item description */
39
- description: string;
40
- /** Status labels */
41
- statusActive: string;
42
- statusInactive: string;
43
- statusExpired: string;
44
- statusCanceled: string;
45
- /** Trial status label (defaults to statusActive if not provided) */
46
- statusTrial?: string;
47
- /** Trial canceled status label (defaults to statusCanceled if not provided) */
48
- statusTrialCanceled?: string;
49
- /** Detail screen translations */
50
- statusLabel: string;
51
- expiresLabel: string;
52
- purchasedLabel: string;
53
- lifetimeLabel: string;
54
- creditsTitle: string;
55
- remainingLabel: string;
56
- manageButton: string;
57
- upgradeButton: string;
58
- /** Credit label (e.g., "Credits") */
59
- creditsLabel: string;
60
- }
61
-
62
- /** Parameters for useSubscriptionSettingsConfig hook */
63
- export interface UseSubscriptionSettingsConfigParams {
64
- /** User ID (required for credits lookup) */
65
- userId?: string;
66
- /** Whether user is anonymous */
67
- isAnonymous?: boolean;
68
- /** Translation strings */
69
- translations: SubscriptionSettingsTranslations;
70
- /** Fixed credit limit (if not available in UserCredits) */
71
- creditLimit?: number;
72
- /** Upgrade prompt configuration for free users */
73
- upgradePrompt?: UpgradePromptConfig;
74
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * useAuthSubscriptionSync Hook
3
- * Single source of truth for RevenueCat initialization
4
- * Handles initial setup and auth state transitions
5
- * Generic implementation for 100+ apps with auth provider abstraction
6
- */
7
-
8
- import { useEffect, useRef, useCallback } from "react";
9
-
10
- export interface AuthSubscriptionSyncConfig {
11
- /** Function to subscribe to auth state changes - returns unsubscribe function */
12
- onAuthStateChanged: (callback: (userId: string | null) => void) => () => void;
13
- /** Function to initialize subscription for a user */
14
- initializeSubscription: (userId: string) => Promise<void>;
15
- }
16
-
17
- export function useAuthSubscriptionSync(
18
- config: AuthSubscriptionSyncConfig,
19
- ): void {
20
- const { onAuthStateChanged, initializeSubscription } = config;
21
- const previousUserIdRef = useRef<string | null>(null);
22
- const isInitializedRef = useRef(false);
23
-
24
- const initialize = useCallback(
25
- async (userId: string) => {
26
- await initializeSubscription(userId);
27
- },
28
- [initializeSubscription],
29
- );
30
-
31
- useEffect(() => {
32
- const unsubscribe = onAuthStateChanged(async (userId: string | null) => {
33
- if (!userId) {
34
- previousUserIdRef.current = null;
35
- return;
36
- }
37
-
38
- const previousUserId = previousUserIdRef.current;
39
-
40
- if (userId === previousUserId) {
41
- return;
42
- }
43
-
44
- try {
45
- if (previousUserId && previousUserId !== userId) {
46
- await initialize(userId);
47
- } else if (!isInitializedRef.current) {
48
- await initialize(userId);
49
- isInitializedRef.current = true;
50
- }
51
- } catch (error) {
52
- // Log error for debugging but don't crash the auth flow
53
- if (__DEV__) {
54
- console.error('[useAuthSubscriptionSync] Initialization failed:', error);
55
- }
56
- }
57
-
58
- previousUserIdRef.current = userId;
59
- });
60
-
61
- return () => unsubscribe();
62
- }, [onAuthStateChanged, initialize]);
63
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * usePremiumGate Hook
3
- *
4
- * Simplified hook for premium-only apps (no credit system).
5
- * Provides screen-level and action-level premium gates.
6
- *
7
- * @example Screen-Level Gate
8
- * ```tsx
9
- * const { isPremium, requireScreen } = usePremiumGate();
10
- *
11
- * useEffect(() => {
12
- * requireScreen(); // Auto-opens paywall if not premium
13
- * }, [requireScreen]);
14
- *
15
- * if (!isPremium) return null;
16
- * ```
17
- *
18
- * @example Action-Level Gate
19
- * ```tsx
20
- * const { requirePremium } = usePremiumGate();
21
- *
22
- * const handleAction = () => {
23
- * requirePremium(() => {
24
- * // Action code here
25
- * });
26
- * };
27
- * ```
28
- */
29
-
30
- import { useCallback } from "react";
31
- import { useSubscriptionStatus } from "./useSubscriptionStatus";
32
- import { paywallControl } from "./usePaywallVisibility";
33
-
34
- export interface UsePremiumGateResult {
35
- /** Whether user has premium access */
36
- isPremium: boolean;
37
- /** Whether subscription status is loading */
38
- isLoading: boolean;
39
- /** Action-level gate: runs callback only if user has premium */
40
- requirePremium: (onSuccess: () => void) => void;
41
- /** Screen-level gate: opens paywall if not premium */
42
- requireScreen: () => void;
43
- }
44
-
45
- export const usePremiumGate = (): UsePremiumGateResult => {
46
- const { isPremium, isLoading } = useSubscriptionStatus();
47
-
48
- const requirePremium = useCallback(
49
- (onSuccess: () => void) => {
50
- if (isLoading) {
51
- return;
52
- }
53
-
54
- if (isPremium) {
55
- onSuccess();
56
- return;
57
- }
58
-
59
- paywallControl.open();
60
- },
61
- [isPremium, isLoading]
62
- );
63
-
64
- const requireScreen = useCallback(() => {
65
- if (!isLoading && !isPremium) {
66
- paywallControl.open();
67
- }
68
- }, [isPremium, isLoading]);
69
-
70
- return {
71
- isPremium,
72
- isLoading,
73
- requirePremium,
74
- requireScreen,
75
- };
76
- };
77
-
78
- /**
79
- * useSubscription Hook (Alias for usePremiumGate)
80
- *
81
- * Simpler name for premium-only apps.
82
- * Same functionality as usePremiumGate.
83
- */
84
- export const useSubscription = usePremiumGate;
@@ -1,148 +0,0 @@
1
- /**
2
- * Saved Purchase Auto-Execution Hook
3
- * Automatically executes saved purchase when user converts from anonymous to authenticated
4
- */
5
-
6
- import { useEffect, useRef } from "react";
7
- import {
8
- useAuthStore,
9
- selectUserId,
10
- selectIsAnonymous,
11
- } from "@umituz/react-native-auth";
12
- import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
13
- import { usePremium } from "./usePremium";
14
- import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
15
- import { usePurchaseLoadingStore } from "./stores";
16
-
17
- export interface UseSavedPurchaseAutoExecutionParams {
18
- onSuccess?: () => void;
19
- onError?: (error: Error) => void;
20
- }
21
-
22
- export interface UseSavedPurchaseAutoExecutionResult {
23
- isExecuting: boolean;
24
- }
25
-
26
- export const useSavedPurchaseAutoExecution = (
27
- params?: UseSavedPurchaseAutoExecutionParams
28
- ): UseSavedPurchaseAutoExecutionResult => {
29
- const { onSuccess, onError } = params ?? {};
30
-
31
- const userId = useAuthStore(selectUserId);
32
- const isAnonymous = useAuthStore(selectIsAnonymous);
33
-
34
- const { purchasePackage } = usePremium();
35
- const { startPurchase, endPurchase } = usePurchaseLoadingStore();
36
-
37
- const prevIsAnonymousRef = useRef<boolean | undefined>(undefined);
38
- const isExecutingRef = useRef(false);
39
- const hasExecutedRef = useRef(false);
40
-
41
- const purchasePackageRef = useRef(purchasePackage);
42
- const onSuccessRef = useRef(onSuccess);
43
- const onErrorRef = useRef(onError);
44
- const startPurchaseRef = useRef(startPurchase);
45
- const endPurchaseRef = useRef(endPurchase);
46
-
47
- // Consolidate all ref updates into a single effect
48
- useEffect(() => {
49
- purchasePackageRef.current = purchasePackage;
50
- onSuccessRef.current = onSuccess;
51
- onErrorRef.current = onError;
52
- startPurchaseRef.current = startPurchase;
53
- endPurchaseRef.current = endPurchase;
54
- }, [purchasePackage, onSuccess, onError, startPurchase, endPurchase]);
55
-
56
- useEffect(() => {
57
- const isAuthenticated = !!userId && !isAnonymous;
58
- const prevIsAnonymous = prevIsAnonymousRef.current;
59
- const savedPurchase = getSavedPurchase();
60
-
61
- const wasAnonymous = prevIsAnonymous === true;
62
- const becameAuthenticated = wasAnonymous && isAuthenticated;
63
-
64
- const shouldLog = prevIsAnonymousRef.current !== isAnonymous;
65
-
66
- if (typeof __DEV__ !== "undefined" && __DEV__ && shouldLog) {
67
- console.log("[SavedPurchaseAutoExecution] Auth state check:", {
68
- userId: userId?.slice(0, 8),
69
- prevIsAnonymous,
70
- isAnonymous,
71
- isAuthenticated,
72
- wasAnonymous,
73
- becameAuthenticated,
74
- hasSavedPurchase: !!savedPurchase,
75
- savedProductId: savedPurchase?.pkg.product.identifier,
76
- willExecute:
77
- becameAuthenticated &&
78
- !!savedPurchase &&
79
- !isExecutingRef.current &&
80
- !hasExecutedRef.current,
81
- });
82
- }
83
-
84
- if (
85
- becameAuthenticated &&
86
- savedPurchase &&
87
- !isExecutingRef.current &&
88
- !hasExecutedRef.current
89
- ) {
90
- hasExecutedRef.current = true;
91
- isExecutingRef.current = true;
92
-
93
- const executeFlow = async () => {
94
- const currentUserId = userId;
95
- if (!currentUserId) {
96
- isExecutingRef.current = false;
97
- return;
98
- }
99
-
100
- const maxAttempts = 20;
101
- const delayMs = 500;
102
-
103
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
104
- const isReady = SubscriptionManager.isInitializedForUser(currentUserId);
105
-
106
- if (isReady) {
107
- const pkg = savedPurchase.pkg;
108
-
109
- startPurchaseRef.current(pkg.product.identifier, "auto-execution");
110
-
111
- try {
112
- const success = await purchasePackageRef.current(pkg);
113
-
114
- if (success) {
115
- clearSavedPurchase();
116
- if (onSuccessRef.current) {
117
- onSuccessRef.current();
118
- }
119
- }
120
- } catch (error) {
121
- if (onErrorRef.current && error instanceof Error) {
122
- onErrorRef.current(error);
123
- }
124
- } finally {
125
- endPurchaseRef.current();
126
- isExecutingRef.current = false;
127
- }
128
-
129
- return;
130
- }
131
-
132
- await new Promise((resolve) => setTimeout(resolve, delayMs));
133
- }
134
-
135
- clearSavedPurchase();
136
- isExecutingRef.current = false;
137
- };
138
-
139
- executeFlow();
140
- }
141
-
142
- prevIsAnonymousRef.current = isAnonymous;
143
- }, [userId, isAnonymous]);
144
-
145
- return {
146
- isExecuting: isExecutingRef.current,
147
- };
148
- };