@umituz/react-native-subscription 2.27.2 → 2.27.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.27.2",
3
+ "version": "2.27.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",
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * useCredits Hook
3
3
  *
4
- * Fetches user credits - NO CACHE, always fresh data.
5
- * Auth info automatically read from @umituz/react-native-auth.
4
+ * Fetches user credits with TanStack Query best practices.
5
+ * Uses status-based state management for reliable loading detection.
6
6
  * Auto-initializes free credits for registered users only.
7
7
  */
8
8
 
9
9
  import { useQuery } from "@umituz/react-native-design-system";
10
- import { useCallback, useMemo, useEffect } from "react";
10
+ import { useCallback, useMemo, useEffect, useState } from "react";
11
11
  import {
12
12
  useAuthStore,
13
13
  selectUserId,
@@ -29,9 +29,13 @@ export const creditsQueryKeys = {
29
29
 
30
30
  const freeCreditsInitAttempted = new Set<string>();
31
31
 
32
+ export type CreditsLoadStatus = "idle" | "loading" | "initializing" | "ready" | "error";
33
+
32
34
  export interface UseCreditsResult {
33
35
  credits: UserCredits | null;
34
36
  isLoading: boolean;
37
+ isCreditsLoaded: boolean;
38
+ loadStatus: CreditsLoadStatus;
35
39
  error: Error | null;
36
40
  hasCredits: boolean;
37
41
  creditsPercent: number;
@@ -39,24 +43,36 @@ export interface UseCreditsResult {
39
43
  canAfford: (cost: number) => boolean;
40
44
  }
41
45
 
46
+ function deriveLoadStatus(
47
+ queryStatus: "pending" | "error" | "success",
48
+ isInitializing: boolean,
49
+ queryEnabled: boolean
50
+ ): CreditsLoadStatus {
51
+ if (!queryEnabled) return "idle";
52
+ if (queryStatus === "pending") return "loading";
53
+ if (queryStatus === "error") return "error";
54
+ if (isInitializing) return "initializing";
55
+ return "ready";
56
+ }
57
+
42
58
  export const useCredits = (): UseCreditsResult => {
43
59
  const userId = useAuthStore(selectUserId);
44
60
  const isAnonymous = useAuthStore(selectIsAnonymous);
45
61
  const isRegisteredUser = !!userId && !isAnonymous;
62
+ const [isInitializingFreeCredits, setIsInitializingFreeCredits] = useState(false);
46
63
 
47
64
  const isConfigured = isCreditsRepositoryConfigured();
48
65
  const config = getCreditsConfig();
49
-
50
66
  const queryEnabled = !!userId && isConfigured;
51
67
 
52
- const { data, isLoading, error, refetch, isFetched } = useQuery({
68
+ const { data, status, error, refetch } = useQuery({
53
69
  queryKey: creditsQueryKeys.user(userId ?? ""),
54
70
  queryFn: async () => {
55
- if (!userId || !isConfigured) {
56
- return null;
57
- }
71
+ if (!userId || !isConfigured) return null;
72
+
58
73
  const repository = getCreditsRepository();
59
74
  const result = await repository.getCredits(userId);
75
+
60
76
  if (!result.success) {
61
77
  throw new Error(result.error?.message || "Failed to fetch credits");
62
78
  }
@@ -80,67 +96,69 @@ export const useCredits = (): UseCreditsResult => {
80
96
  });
81
97
 
82
98
  const credits = data ?? null;
83
-
84
99
  const freeCredits = config.freeCredits ?? 0;
85
100
  const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
101
+ const querySuccess = status === "success";
86
102
 
87
103
  useEffect(() => {
88
- if (
89
- isFetched &&
104
+ const shouldInitFreeCredits =
105
+ querySuccess &&
90
106
  userId &&
91
107
  isRegisteredUser &&
92
108
  isConfigured &&
93
109
  !credits &&
94
110
  autoInit &&
95
- !freeCreditsInitAttempted.has(userId)
96
- ) {
111
+ !freeCreditsInitAttempted.has(userId);
112
+
113
+ if (shouldInitFreeCredits) {
97
114
  freeCreditsInitAttempted.add(userId);
115
+ setIsInitializingFreeCredits(true);
98
116
 
99
117
  if (typeof __DEV__ !== "undefined" && __DEV__) {
100
- console.log("[useCredits] Initializing free credits for registered user:", userId.slice(0, 8));
118
+ console.log("[useCredits] Initializing free credits:", userId.slice(0, 8));
101
119
  }
102
120
 
103
121
  const repository = getCreditsRepository();
104
122
  repository.initializeFreeCredits(userId).then((result) => {
123
+ setIsInitializingFreeCredits(false);
124
+
105
125
  if (result.success) {
106
126
  if (typeof __DEV__ !== "undefined" && __DEV__) {
107
127
  console.log("[useCredits] Free credits initialized:", result.data?.credits);
108
128
  }
109
129
  refetch();
110
- } else {
111
- if (typeof __DEV__ !== "undefined" && __DEV__) {
112
- console.warn("[useCredits] Free credits init failed:", result.error?.message);
113
- }
130
+ } else if (typeof __DEV__ !== "undefined" && __DEV__) {
131
+ console.warn("[useCredits] Free credits init failed:", result.error?.message);
114
132
  }
115
133
  });
116
- } else if (isFetched && userId && isAnonymous && !credits && autoInit) {
134
+ } else if (querySuccess && userId && isAnonymous && !credits && autoInit) {
117
135
  if (typeof __DEV__ !== "undefined" && __DEV__) {
118
136
  console.log("[useCredits] Skipping free credits - anonymous user must register first");
119
137
  }
120
138
  }
121
- }, [isFetched, userId, isRegisteredUser, isAnonymous, isConfigured, credits, autoInit, refetch]);
139
+ }, [querySuccess, userId, isRegisteredUser, isAnonymous, isConfigured, credits, autoInit, refetch]);
122
140
 
123
141
  const derivedValues = useMemo(() => {
124
142
  const has = (credits?.credits ?? 0) > 0;
125
- const percent = credits
126
- ? Math.round((credits.credits / config.creditLimit) * 100)
127
- : 0;
128
-
143
+ const percent = credits ? Math.round((credits.credits / config.creditLimit) * 100) : 0;
129
144
  return { hasCredits: has, creditsPercent: percent };
130
145
  }, [credits, config.creditLimit]);
131
146
 
132
147
  const canAfford = useCallback(
133
- (cost: number): boolean => {
134
- if (!credits) return false;
135
- return credits.credits >= cost;
136
- },
148
+ (cost: number): boolean => (credits?.credits ?? 0) >= cost,
137
149
  [credits]
138
150
  );
139
151
 
152
+ const loadStatus = deriveLoadStatus(status, isInitializingFreeCredits, queryEnabled);
153
+ const isCreditsLoaded = loadStatus === "ready";
154
+ const isLoading = loadStatus === "loading" || loadStatus === "initializing";
155
+
140
156
  return {
141
157
  credits,
142
158
  isLoading,
143
- error: error as Error | null,
159
+ isCreditsLoaded,
160
+ loadStatus,
161
+ error: error instanceof Error ? error : null,
144
162
  hasCredits: derivedValues.hasCredits,
145
163
  creditsPercent: derivedValues.creditsPercent,
146
164
  refetch,
@@ -2,6 +2,7 @@
2
2
  * useFeatureGate Hook
3
3
  * Unified feature gate: Auth → Subscription → Credits
4
4
  * Uses ref pattern to avoid stale closure issues.
5
+ * Event-driven approach - no polling, no waiting.
5
6
  */
6
7
 
7
8
  import { useCallback, useRef, useEffect } from "react";
@@ -15,6 +16,7 @@ export interface UseFeatureGateParams {
15
16
  readonly creditBalance: number;
16
17
  readonly requiredCredits?: number;
17
18
  readonly onShowPaywall: (requiredCredits?: number) => void;
19
+ readonly isCreditsLoaded?: boolean;
18
20
  }
19
21
 
20
22
  export interface UseFeatureGateResult {
@@ -34,15 +36,18 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
34
36
  creditBalance,
35
37
  requiredCredits = 1,
36
38
  onShowPaywall,
39
+ isCreditsLoaded = true,
37
40
  } = params;
38
41
 
39
42
  const pendingActionRef = useRef<(() => void | Promise<void>) | null>(null);
40
43
  const prevCreditBalanceRef = useRef(creditBalance);
41
44
  const isWaitingForPurchaseRef = useRef(false);
45
+ const isWaitingForAuthCreditsRef = useRef(false);
42
46
 
43
47
  const creditBalanceRef = useRef(creditBalance);
44
48
  const hasSubscriptionRef = useRef(hasSubscription);
45
49
  const onShowPaywallRef = useRef(onShowPaywall);
50
+ const requiredCreditsRef = useRef(requiredCredits);
46
51
 
47
52
  useEffect(() => {
48
53
  creditBalanceRef.current = creditBalance;
@@ -56,6 +61,48 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
56
61
  onShowPaywallRef.current = onShowPaywall;
57
62
  }, [onShowPaywall]);
58
63
 
64
+ useEffect(() => {
65
+ requiredCreditsRef.current = requiredCredits;
66
+ }, [requiredCredits]);
67
+
68
+ useEffect(() => {
69
+ if (!isWaitingForAuthCreditsRef.current || !isCreditsLoaded || !pendingActionRef.current) {
70
+ return;
71
+ }
72
+
73
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
74
+ console.log("[useFeatureGate] Credits loaded after auth", {
75
+ credits: creditBalance,
76
+ hasSubscription,
77
+ isCreditsLoaded,
78
+ });
79
+ }
80
+
81
+ isWaitingForAuthCreditsRef.current = false;
82
+
83
+ if (hasSubscription || creditBalance >= requiredCredits) {
84
+ const action = pendingActionRef.current;
85
+ pendingActionRef.current = null;
86
+
87
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
88
+ console.log("[useFeatureGate] Proceeding with action after auth", {
89
+ credits: creditBalance,
90
+ hasSubscription,
91
+ });
92
+ }
93
+ action();
94
+ return;
95
+ }
96
+
97
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
98
+ console.log("[useFeatureGate] No credits after auth, showing paywall", {
99
+ credits: creditBalance,
100
+ });
101
+ }
102
+ isWaitingForPurchaseRef.current = true;
103
+ onShowPaywall(requiredCredits);
104
+ }, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywall]);
105
+
59
106
  useEffect(() => {
60
107
  const prevBalance = prevCreditBalanceRef.current ?? 0;
61
108
  const creditsIncreased = creditBalance > prevBalance;
@@ -82,57 +129,18 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
82
129
  hasSubscription,
83
130
  creditBalance: creditBalanceRef.current,
84
131
  requiredCredits,
132
+ isCreditsLoaded,
85
133
  });
86
134
  }
87
135
 
88
136
  if (!isAuthenticated) {
89
- const postAuthAction = async () => {
90
- // Wait for free credits to initialize after registration (max 3 seconds)
91
- const maxWaitTime = 3000;
92
- const checkInterval = 100;
93
- let waited = 0;
94
-
95
- while (waited < maxWaitTime) {
96
- await new Promise((resolve) => setTimeout(resolve, checkInterval));
97
- waited += checkInterval;
98
-
99
- if (creditBalanceRef.current > 0 || hasSubscriptionRef.current) {
100
- if (typeof __DEV__ !== "undefined" && __DEV__) {
101
- console.log("[useFeatureGate] Credits/subscription detected after auth", {
102
- credits: creditBalanceRef.current,
103
- hasSubscription: hasSubscriptionRef.current,
104
- waitedMs: waited,
105
- });
106
- }
107
- break;
108
- }
109
- }
110
-
111
- if (hasSubscriptionRef.current) {
112
- action();
113
- return;
114
- }
115
-
116
- const currentBalance = creditBalanceRef.current;
117
- if (currentBalance < requiredCredits) {
118
- if (typeof __DEV__ !== "undefined" && __DEV__) {
119
- console.log("[useFeatureGate] No credits after waiting, showing paywall", {
120
- credits: currentBalance,
121
- waitedMs: waited,
122
- });
123
- }
124
- pendingActionRef.current = action;
125
- isWaitingForPurchaseRef.current = true;
126
- onShowPaywallRef.current(requiredCredits);
127
- return;
128
- }
137
+ const postAuthAction = () => {
138
+ pendingActionRef.current = action;
139
+ isWaitingForAuthCreditsRef.current = true;
129
140
 
130
141
  if (typeof __DEV__ !== "undefined" && __DEV__) {
131
- console.log("[useFeatureGate] Proceeding with action after auth", {
132
- credits: currentBalance,
133
- });
142
+ console.log("[useFeatureGate] Auth completed, waiting for credits to load");
134
143
  }
135
- action();
136
144
  };
137
145
  onShowAuthModal(postAuthAction);
138
146
  return;
@@ -156,7 +164,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
156
164
 
157
165
  action();
158
166
  },
159
- [isAuthenticated, hasSubscription, requiredCredits, onShowAuthModal, onShowPaywall]
167
+ [isAuthenticated, hasSubscription, requiredCredits, onShowAuthModal, onShowPaywall, isCreditsLoaded]
160
168
  );
161
169
 
162
170
  const hasCredits = creditBalance >= requiredCredits;