@umituz/react-native-subscription 2.43.2 → 2.43.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.43.2",
3
+ "version": "2.43.4",
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",
@@ -6,9 +6,9 @@ import {
6
6
  selectUserId,
7
7
  } from "@umituz/react-native-auth";
8
8
  import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
9
- import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
10
9
  import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
11
10
  import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
11
+ import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
12
12
  import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
13
13
 
14
14
  interface PurchaseMutationResult {
@@ -39,16 +39,12 @@ export const usePurchasePackage = () => {
39
39
  onSuccess: (result) => {
40
40
  if (result.success) {
41
41
  showSuccess("Purchase Successful", "Your subscription is now active!");
42
- queryClient.invalidateQueries({
43
- queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
44
- });
42
+
43
+ // Invalidate caches after successful purchase
44
+ queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
45
45
  if (userId) {
46
- queryClient.invalidateQueries({
47
- queryKey: subscriptionStatusQueryKeys.user(userId),
48
- });
49
- queryClient.invalidateQueries({
50
- queryKey: creditsQueryKeys.user(userId),
51
- });
46
+ queryClient.invalidateQueries({ queryKey: subscriptionStatusQueryKeys.user(userId) });
47
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
52
48
  }
53
49
  } else {
54
50
  showError("Purchase Failed", "Unable to complete purchase. Please try again.");
@@ -5,9 +5,9 @@ import {
5
5
  selectUserId,
6
6
  } from "@umituz/react-native-auth";
7
7
  import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
8
- import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
9
8
  import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionStatus";
10
9
  import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
10
+ import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
11
11
  import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
12
12
 
13
13
  interface RestoreResult {
@@ -31,16 +31,11 @@ export const useRestorePurchase = () => {
31
31
  },
32
32
  onSuccess: (result) => {
33
33
  if (result.success) {
34
- queryClient.invalidateQueries({
35
- queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
36
- });
34
+ // Invalidate caches after successful restore
35
+ queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
37
36
  if (userId) {
38
- queryClient.invalidateQueries({
39
- queryKey: subscriptionStatusQueryKeys.user(userId),
40
- });
41
- queryClient.invalidateQueries({
42
- queryKey: creditsQueryKeys.user(userId),
43
- });
37
+ queryClient.invalidateQueries({ queryKey: subscriptionStatusQueryKeys.user(userId) });
38
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
44
39
  }
45
40
 
46
41
  if (result.productId) {
@@ -1,50 +1,53 @@
1
- import type { MutableRefObject } from "react";
1
+ import type { FeatureGateState } from "./hooks/useFeatureGateState";
2
2
 
3
- export const executeFeatureAction = (
3
+ /**
4
+ * Executes a feature action with proper auth, subscription, and credit checks.
5
+ * Queues the action if waiting for auth completion or purchase flow.
6
+ *
7
+ * @param action - The action to execute when conditions are met
8
+ * @param isAuthenticated - Whether user is authenticated
9
+ * @param onShowAuthModal - Callback to show auth modal
10
+ * @param state - Feature gate state containing all refs and flags
11
+ * @returns true if action was executed immediately, false if queued/pending
12
+ */
13
+ export function executeFeatureAction(
4
14
  action: () => void | Promise<void>,
5
15
  isAuthenticated: boolean,
6
16
  onShowAuthModal: (callback: () => void | Promise<void>) => void,
7
- hasSubscriptionRef: MutableRefObject<boolean>,
8
- creditBalanceRef: MutableRefObject<number>,
9
- requiredCreditsRef: MutableRefObject<number>,
10
- onShowPaywallRef: MutableRefObject<(requiredCredits?: number) => void>,
11
- pendingActionRef: MutableRefObject<(() => void | Promise<void>) | null>,
12
- isWaitingForAuthCreditsRef: MutableRefObject<boolean>,
13
- isWaitingForPurchaseRef: MutableRefObject<boolean>,
14
- isCreditsLoadedRef: MutableRefObject<boolean>
15
- ): boolean => {
17
+ state: FeatureGateState
18
+ ): boolean {
16
19
 
17
20
  if (!isAuthenticated) {
18
21
  const postAuthAction = () => {
19
- if (hasSubscriptionRef.current || creditBalanceRef.current >= requiredCreditsRef.current) {
22
+ if (state.hasSubscriptionRef.current || state.creditBalanceRef.current >= state.requiredCreditsRef.current) {
20
23
  action();
21
24
  return;
22
25
  }
23
26
 
24
- if (isCreditsLoadedRef.current) {
25
- pendingActionRef.current = action;
26
- isWaitingForPurchaseRef.current = true;
27
- onShowPaywallRef.current(requiredCreditsRef.current);
27
+ if (state.isCreditsLoadedRef.current) {
28
+ state.pendingActionRef.current = action;
29
+ state.isWaitingForPurchaseRef.current = true;
30
+ state.onShowPaywallRef.current(state.requiredCreditsRef.current);
28
31
  return;
29
32
  }
30
- pendingActionRef.current = action;
31
- isWaitingForAuthCreditsRef.current = true;
33
+ state.pendingActionRef.current = action;
34
+ state.isWaitingForAuthCreditsRef.current = true;
32
35
  };
33
36
  onShowAuthModal(postAuthAction);
34
37
  return false;
35
38
  }
36
39
 
37
- if (hasSubscriptionRef.current) {
40
+ if (state.hasSubscriptionRef.current) {
38
41
  action();
39
42
  return true;
40
43
  }
41
44
 
42
- if (creditBalanceRef.current < requiredCreditsRef.current) {
43
- pendingActionRef.current = action;
44
- isWaitingForPurchaseRef.current = true;
45
- onShowPaywallRef.current(requiredCreditsRef.current);
45
+ if (state.creditBalanceRef.current < state.requiredCreditsRef.current) {
46
+ state.pendingActionRef.current = action;
47
+ state.isWaitingForPurchaseRef.current = true;
48
+ state.onShowPaywallRef.current(state.requiredCreditsRef.current);
46
49
  return false;
47
50
  }
48
51
  action();
49
52
  return true;
50
- };
53
+ }
@@ -0,0 +1,76 @@
1
+ import { useRef } from "react";
2
+ import type { UseFeatureGateParams } from "../useFeatureGate.types";
3
+
4
+ /**
5
+ * Internal state management for useFeatureGate hook.
6
+ * Encapsulates all refs and state to reduce parameter passing.
7
+ */
8
+ export interface FeatureGateState {
9
+ // Action queue
10
+ pendingActionRef: React.MutableRefObject<(() => void | Promise<void>) | null>;
11
+
12
+ // Previous values for change detection
13
+ prevCreditBalanceRef: React.MutableRefObject<number | undefined>;
14
+ prevHasSubscriptionRef: React.MutableRefObject<boolean>;
15
+
16
+ // Waiting flags for async operations
17
+ isWaitingForAuthCreditsRef: React.MutableRefObject<boolean>;
18
+ isWaitingForPurchaseRef: React.MutableRefObject<boolean>;
19
+
20
+ // Live refs (synced with current values)
21
+ creditBalanceRef: React.MutableRefObject<number>;
22
+ hasSubscriptionRef: React.MutableRefObject<boolean>;
23
+ onShowPaywallRef: React.MutableRefObject<(requiredCredits?: number) => void>;
24
+ requiredCreditsRef: React.MutableRefObject<number>;
25
+ isCreditsLoadedRef: React.MutableRefObject<boolean>;
26
+ }
27
+
28
+ /**
29
+ * Creates and initializes all refs for useFeatureGate.
30
+ * This encapsulates ref creation and initialization logic.
31
+ */
32
+ export function useFeatureGateRefs(params: UseFeatureGateParams): FeatureGateState {
33
+ const {
34
+ creditBalance,
35
+ hasSubscription = false,
36
+ onShowPaywall,
37
+ requiredCredits = 1,
38
+ isCreditsLoaded = true,
39
+ } = params;
40
+
41
+ const pendingActionRef = useRef<(() => void | Promise<void>) | null>(null);
42
+ const prevCreditBalanceRef = useRef(creditBalance);
43
+ const prevHasSubscriptionRef = useRef(hasSubscription);
44
+ const isWaitingForAuthCreditsRef = useRef(false);
45
+ const isWaitingForPurchaseRef = useRef(false);
46
+
47
+ return {
48
+ pendingActionRef,
49
+ prevCreditBalanceRef,
50
+ prevHasSubscriptionRef,
51
+ isWaitingForAuthCreditsRef,
52
+ isWaitingForPurchaseRef,
53
+ creditBalanceRef: useRef(creditBalance),
54
+ hasSubscriptionRef: useRef(hasSubscription),
55
+ onShowPaywallRef: useRef(onShowPaywall),
56
+ requiredCreditsRef: useRef(requiredCredits),
57
+ isCreditsLoadedRef: useRef(isCreditsLoaded),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Updates live refs when their source values change.
63
+ * Call this in a useEffect or when values update.
64
+ */
65
+ export function updateLiveRefs(
66
+ state: FeatureGateState,
67
+ params: Pick<UseFeatureGateParams, 'creditBalance' | 'hasSubscription' | 'onShowPaywall' | 'requiredCredits' | 'isCreditsLoaded'>
68
+ ): void {
69
+ const { creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded } = params;
70
+
71
+ state.creditBalanceRef.current = creditBalance;
72
+ state.hasSubscriptionRef.current = hasSubscription;
73
+ state.onShowPaywallRef.current = onShowPaywall;
74
+ state.requiredCreditsRef.current = requiredCredits;
75
+ state.isCreditsLoadedRef.current = isCreditsLoaded;
76
+ }
@@ -3,6 +3,7 @@ import type { PurchasesPackage } from "react-native-purchases";
3
3
  import { usePremium } from "./usePremium";
4
4
  import type { PurchaseSource } from "../core/SubscriptionConstants";
5
5
  import { authPurchaseStateManager } from "../infrastructure/utils/authPurchaseState";
6
+ import { requireAuthentication } from "./utils/authCheckUtils";
6
7
 
7
8
  export const configureAuthProvider = (provider: import("../infrastructure/utils/authPurchaseState").PurchaseAuthProvider): void => {
8
9
  authPurchaseStateManager.configure(provider);
@@ -26,6 +27,10 @@ interface UseAuthAwarePurchaseResult {
26
27
  executeSavedPurchase: () => Promise<boolean>;
27
28
  }
28
29
 
30
+ /**
31
+ * Hook for purchase operations that handle authentication.
32
+ * Automatically saves pending purchases and shows auth modal when needed.
33
+ */
29
34
  export const useAuthAwarePurchase = (
30
35
  params?: UseAuthAwarePurchaseParams
31
36
  ): UseAuthAwarePurchaseResult => {
@@ -50,11 +55,10 @@ export const useAuthAwarePurchase = (
50
55
  }
51
56
  }, [purchasePackage]);
52
57
 
58
+ // Auto-execute saved purchase when user authenticates
53
59
  useEffect(() => {
54
60
  const authProvider = authPurchaseStateManager.getProvider();
55
- if (!authProvider) return;
56
-
57
- const hasUser = authProvider.hasFirebaseUser();
61
+ const hasUser = authProvider && authProvider.hasFirebaseUser();
58
62
  const hasSavedPurchase = !!authPurchaseStateManager.getSavedPurchase();
59
63
 
60
64
  if (hasUser && hasSavedPurchase && !isExecutingSavedRef.current) {
@@ -69,18 +73,14 @@ export const useAuthAwarePurchase = (
69
73
  async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
70
74
  const authProvider = authPurchaseStateManager.getProvider();
71
75
 
72
- if (!authProvider) {
73
- return false;
74
- }
75
-
76
- if (!authProvider.hasFirebaseUser()) {
76
+ if (!requireAuthentication(authProvider)) {
77
+ // User not authenticated, purchase saved and auth modal shown
77
78
  authPurchaseStateManager.savePurchase(pkg, source || params?.source || "settings");
78
- authProvider.showAuthModal();
79
79
  return false;
80
80
  }
81
81
 
82
+ // User authenticated, proceed with purchase
82
83
  const result = await purchasePackage(pkg);
83
-
84
84
  return result;
85
85
  },
86
86
  [purchasePackage, params?.source]
@@ -89,17 +89,11 @@ export const useAuthAwarePurchase = (
89
89
  const handleRestore = useCallback(async (): Promise<boolean> => {
90
90
  const authProvider = authPurchaseStateManager.getProvider();
91
91
 
92
- if (!authProvider) {
93
- return false;
94
- }
95
-
96
- if (!authProvider.hasFirebaseUser()) {
97
- authProvider.showAuthModal();
92
+ if (!requireAuthentication(authProvider)) {
98
93
  return false;
99
94
  }
100
95
 
101
96
  const result = await restorePurchase();
102
-
103
97
  return result;
104
98
  }, [restorePurchase]);
105
99
 
@@ -1,8 +1,8 @@
1
- import { useCallback, useRef, useEffect } from "react";
1
+ import { useCallback, useEffect } from "react";
2
2
  import type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
3
3
  import { DEFAULT_REQUIRED_CREDITS, canExecuteAuthAction, canExecutePurchaseAction } from "../application/featureGate/featureGateBusinessRules";
4
- import { useSyncedRefs } from "./featureGateRefs";
5
4
  import { executeFeatureAction } from "./featureGateActions";
5
+ import { useFeatureGateRefs, updateLiveRefs } from "./hooks/useFeatureGateState";
6
6
 
7
7
  export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
8
8
  const {
@@ -11,73 +11,64 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
11
11
  hasSubscription = false,
12
12
  creditBalance,
13
13
  requiredCredits = DEFAULT_REQUIRED_CREDITS,
14
- onShowPaywall,
15
14
  isCreditsLoaded = true,
16
15
  } = params;
17
16
 
18
- const pendingActionRef = useRef<(() => void | Promise<void>) | null>(null);
19
- const prevCreditBalanceRef = useRef(creditBalance);
20
- // Separate ref to track previous subscription state for canExecutePurchaseAction.
21
- // NOTE: Must NOT use hasSubscriptionRef from useSyncedRefs here because useSyncedRefs
22
- // effects run BEFORE this effect (React runs effects in definition order), so
23
- // hasSubscriptionRef.current would already be the NEW value when we check it.
24
- const prevHasSubscriptionRef = useRef(hasSubscription);
25
- const isWaitingForPurchaseRef = useRef(false);
26
- const isWaitingForAuthCreditsRef = useRef(false);
17
+ const state = useFeatureGateRefs(params);
27
18
 
28
- const { creditBalanceRef, hasSubscriptionRef, onShowPaywallRef, requiredCreditsRef, isCreditsLoadedRef } = useSyncedRefs(creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded);
19
+ // Update live refs when params change
20
+ useEffect(() => {
21
+ updateLiveRefs(state, params);
22
+ });
29
23
 
24
+ // Handle post-auth credit loading and action execution
30
25
  useEffect(() => {
31
26
  const shouldExecute = canExecuteAuthAction(
32
- isWaitingForAuthCreditsRef.current,
27
+ state.isWaitingForAuthCreditsRef.current,
33
28
  isCreditsLoaded,
34
- !!pendingActionRef.current,
29
+ !!state.pendingActionRef.current,
35
30
  hasSubscription,
36
31
  creditBalance,
37
32
  requiredCredits
38
33
  );
39
34
 
40
35
  if (shouldExecute) {
41
- isWaitingForAuthCreditsRef.current = false;
42
- const action = pendingActionRef.current!;
43
- pendingActionRef.current = null;
36
+ state.isWaitingForAuthCreditsRef.current = false;
37
+ const action = state.pendingActionRef.current!;
38
+ state.pendingActionRef.current = null;
44
39
  action();
45
40
  return;
46
41
  }
47
42
 
48
- if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
49
- isWaitingForAuthCreditsRef.current = false;
50
- isWaitingForPurchaseRef.current = true;
51
- // Use ref to avoid unstable callback dependency
52
- onShowPaywallRef.current(requiredCreditsRef.current);
43
+ if (state.isWaitingForAuthCreditsRef.current && isCreditsLoaded && state.pendingActionRef.current) {
44
+ state.isWaitingForAuthCreditsRef.current = false;
45
+ state.isWaitingForPurchaseRef.current = true;
46
+ state.onShowPaywallRef.current(state.requiredCreditsRef.current);
53
47
  }
54
- // Removed onShowPaywall from dependencies - using ref instead
55
- }, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywallRef, requiredCreditsRef]);
48
+ }, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, state]);
56
49
 
50
+ // Handle post-purchase action execution
57
51
  useEffect(() => {
58
- // Use prevHasSubscriptionRef (updated AFTER check) not hasSubscriptionRef from useSyncedRefs
59
- // (which is already updated to new value before this effect runs - race condition fix)
60
52
  const shouldExecute = canExecutePurchaseAction(
61
- isWaitingForPurchaseRef.current,
53
+ state.isWaitingForPurchaseRef.current,
62
54
  creditBalance,
63
- prevCreditBalanceRef.current ?? 0,
55
+ state.prevCreditBalanceRef.current ?? 0,
64
56
  hasSubscription,
65
- prevHasSubscriptionRef.current,
66
- !!pendingActionRef.current
57
+ state.prevHasSubscriptionRef.current,
58
+ !!state.pendingActionRef.current
67
59
  );
68
60
 
69
61
  if (shouldExecute) {
70
- const action = pendingActionRef.current!;
71
- pendingActionRef.current = null;
72
- isWaitingForPurchaseRef.current = false;
62
+ const action = state.pendingActionRef.current!;
63
+ state.pendingActionRef.current = null;
64
+ state.isWaitingForPurchaseRef.current = false;
73
65
  action();
74
66
  }
75
67
 
76
- // Update AFTER check so next render has correct "prev" values
77
- prevCreditBalanceRef.current = creditBalance;
78
- prevHasSubscriptionRef.current = hasSubscription;
79
-
80
- }, [creditBalance, hasSubscription]);
68
+ // Update prev refs AFTER check for next render
69
+ state.prevCreditBalanceRef.current = creditBalance;
70
+ state.prevHasSubscriptionRef.current = hasSubscription;
71
+ }, [creditBalance, hasSubscription, state]);
81
72
 
82
73
  const requireFeature = useCallback(
83
74
  (action: () => void | Promise<void>): boolean => {
@@ -85,17 +76,10 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
85
76
  action,
86
77
  isAuthenticated,
87
78
  onShowAuthModal,
88
- hasSubscriptionRef,
89
- creditBalanceRef,
90
- requiredCreditsRef,
91
- onShowPaywallRef,
92
- pendingActionRef,
93
- isWaitingForAuthCreditsRef,
94
- isWaitingForPurchaseRef,
95
- isCreditsLoadedRef
79
+ state
96
80
  );
97
81
  },
98
- [isAuthenticated, onShowAuthModal, hasSubscriptionRef, creditBalanceRef, requiredCreditsRef, onShowPaywallRef, isCreditsLoadedRef]
82
+ [isAuthenticated, onShowAuthModal, state]
99
83
  );
100
84
 
101
85
  return {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Authentication check utilities for purchase flows.
3
+ * Extracted to reduce duplication in useAuthAwarePurchase.
4
+ */
5
+
6
+ export interface PurchaseAuthProvider {
7
+ hasFirebaseUser: () => boolean;
8
+ showAuthModal: () => void;
9
+ }
10
+
11
+ /**
12
+ * Checks if auth provider is available and user is authenticated.
13
+ *
14
+ * @param authProvider - Auth provider from authPurchaseStateManager
15
+ * @returns true if user is authenticated, false otherwise
16
+ */
17
+ export function isUserAuthenticated(authProvider: PurchaseAuthProvider | null): boolean {
18
+ if (!authProvider) {
19
+ return false;
20
+ }
21
+ return authProvider.hasFirebaseUser();
22
+ }
23
+
24
+ /**
25
+ * Ensures user is authenticated before proceeding with an action.
26
+ * If not authenticated, shows auth modal and returns false.
27
+ *
28
+ * @param authProvider - Auth provider from authPurchaseStateManager
29
+ * @returns true if authenticated, false if auth modal was shown
30
+ */
31
+ export function requireAuthentication(authProvider: PurchaseAuthProvider | null): boolean {
32
+ if (!authProvider) {
33
+ return false;
34
+ }
35
+
36
+ if (!authProvider.hasFirebaseUser()) {
37
+ authProvider.showAuthModal();
38
+ return false;
39
+ }
40
+
41
+ return true;
42
+ }