@umituz/react-native-subscription 2.27.3 → 2.27.6

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.3",
3
+ "version": "2.27.6",
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.
6
- * Auto-initializes free credits for registered users only.
4
+ * Fetches user credits with TanStack Query best practices.
5
+ * Uses status-based state management for reliable loading detection.
6
+ * Free credits initialization is delegated to useFreeCreditsInit hook.
7
7
  */
8
8
 
9
9
  import { useQuery } from "@umituz/react-native-design-system";
10
- import { useCallback, useMemo, useEffect, useState } from "react";
10
+ import { useCallback, useMemo } from "react";
11
11
  import {
12
12
  useAuthStore,
13
13
  selectUserId,
@@ -19,6 +19,7 @@ import {
19
19
  getCreditsConfig,
20
20
  isCreditsRepositoryConfigured,
21
21
  } from "../../infrastructure/repositories/CreditsRepositoryProvider";
22
+ import { useFreeCreditsInit } from "./useFreeCreditsInit";
22
23
 
23
24
  declare const __DEV__: boolean;
24
25
 
@@ -27,12 +28,13 @@ export const creditsQueryKeys = {
27
28
  user: (userId: string) => ["credits", userId] as const,
28
29
  };
29
30
 
30
- const freeCreditsInitAttempted = new Set<string>();
31
+ export type CreditsLoadStatus = "idle" | "loading" | "initializing" | "ready" | "error";
31
32
 
32
33
  export interface UseCreditsResult {
33
34
  credits: UserCredits | null;
34
35
  isLoading: boolean;
35
36
  isCreditsLoaded: boolean;
37
+ loadStatus: CreditsLoadStatus;
36
38
  error: Error | null;
37
39
  hasCredits: boolean;
38
40
  creditsPercent: number;
@@ -40,25 +42,35 @@ export interface UseCreditsResult {
40
42
  canAfford: (cost: number) => boolean;
41
43
  }
42
44
 
45
+ function deriveLoadStatus(
46
+ queryStatus: "pending" | "error" | "success",
47
+ isInitializing: boolean,
48
+ queryEnabled: boolean
49
+ ): CreditsLoadStatus {
50
+ if (!queryEnabled) return "idle";
51
+ if (queryStatus === "pending") return "loading";
52
+ if (queryStatus === "error") return "error";
53
+ if (isInitializing) return "initializing";
54
+ return "ready";
55
+ }
56
+
43
57
  export const useCredits = (): UseCreditsResult => {
44
58
  const userId = useAuthStore(selectUserId);
45
59
  const isAnonymous = useAuthStore(selectIsAnonymous);
46
60
  const isRegisteredUser = !!userId && !isAnonymous;
47
- const [isInitializingFreeCredits, setIsInitializingFreeCredits] = useState(false);
48
61
 
49
62
  const isConfigured = isCreditsRepositoryConfigured();
50
63
  const config = getCreditsConfig();
51
-
52
64
  const queryEnabled = !!userId && isConfigured;
53
65
 
54
- const { data, isLoading, error, refetch, isFetched } = useQuery({
66
+ const { data, status, error, refetch } = useQuery({
55
67
  queryKey: creditsQueryKeys.user(userId ?? ""),
56
68
  queryFn: async () => {
57
- if (!userId || !isConfigured) {
58
- return null;
59
- }
69
+ if (!userId || !isConfigured) return null;
70
+
60
71
  const repository = getCreditsRepository();
61
72
  const result = await repository.getCredits(userId);
73
+
62
74
  if (!result.success) {
63
75
  throw new Error(result.error?.message || "Failed to fetch credits");
64
76
  }
@@ -82,72 +94,45 @@ export const useCredits = (): UseCreditsResult => {
82
94
  });
83
95
 
84
96
  const credits = data ?? null;
85
-
86
- const freeCredits = config.freeCredits ?? 0;
87
- const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
88
-
89
- useEffect(() => {
90
- if (
91
- isFetched &&
92
- userId &&
93
- isRegisteredUser &&
94
- isConfigured &&
95
- !credits &&
96
- autoInit &&
97
- !freeCreditsInitAttempted.has(userId)
98
- ) {
99
- freeCreditsInitAttempted.add(userId);
100
- setIsInitializingFreeCredits(true);
101
-
102
- if (typeof __DEV__ !== "undefined" && __DEV__) {
103
- console.log("[useCredits] Initializing free credits for registered user:", userId.slice(0, 8));
104
- }
105
-
106
- const repository = getCreditsRepository();
107
- repository.initializeFreeCredits(userId).then((result) => {
108
- setIsInitializingFreeCredits(false);
109
- if (result.success) {
110
- if (typeof __DEV__ !== "undefined" && __DEV__) {
111
- console.log("[useCredits] Free credits initialized:", result.data?.credits);
112
- }
113
- refetch();
114
- } else {
115
- if (typeof __DEV__ !== "undefined" && __DEV__) {
116
- console.warn("[useCredits] Free credits init failed:", result.error?.message);
117
- }
118
- }
119
- });
120
- } else if (isFetched && userId && isAnonymous && !credits && autoInit) {
121
- if (typeof __DEV__ !== "undefined" && __DEV__) {
122
- console.log("[useCredits] Skipping free credits - anonymous user must register first");
123
- }
124
- }
125
- }, [isFetched, userId, isRegisteredUser, isAnonymous, isConfigured, credits, autoInit, refetch]);
97
+ const querySuccess = status === "success";
98
+ const hasCreditsData = (credits?.credits ?? 0) > 0;
99
+
100
+ // Delegate free credits initialization to dedicated hook
101
+ const { isInitializing, needsInit } = useFreeCreditsInit({
102
+ userId,
103
+ isRegisteredUser,
104
+ isAnonymous,
105
+ hasCredits: hasCreditsData,
106
+ querySuccess,
107
+ onInitComplete: refetch,
108
+ });
126
109
 
127
110
  const derivedValues = useMemo(() => {
128
111
  const has = (credits?.credits ?? 0) > 0;
129
- const percent = credits
130
- ? Math.round((credits.credits / config.creditLimit) * 100)
131
- : 0;
132
-
112
+ const percent = credits ? Math.round((credits.credits / config.creditLimit) * 100) : 0;
133
113
  return { hasCredits: has, creditsPercent: percent };
134
114
  }, [credits, config.creditLimit]);
135
115
 
136
116
  const canAfford = useCallback(
137
- (cost: number): boolean => {
138
- if (!credits) return false;
139
- return credits.credits >= cost;
140
- },
117
+ (cost: number): boolean => (credits?.credits ?? 0) >= cost,
141
118
  [credits]
142
119
  );
143
120
 
144
- const isCreditsLoaded = isFetched && !isLoading && !isInitializingFreeCredits;
121
+ // Include needsInit in initializing state for accurate loading detection
122
+ const loadStatus = deriveLoadStatus(
123
+ status,
124
+ isInitializing || needsInit,
125
+ queryEnabled
126
+ );
127
+ const isCreditsLoaded = loadStatus === "ready";
128
+ const isLoading = loadStatus === "loading" || loadStatus === "initializing";
145
129
 
146
130
  return {
147
131
  credits,
148
132
  isLoading,
149
133
  isCreditsLoaded,
150
- error: error as Error | null,
134
+ loadStatus,
135
+ error: error instanceof Error ? error : null,
151
136
  hasCredits: derivedValues.hasCredits,
152
137
  creditsPercent: derivedValues.creditsPercent,
153
138
  refetch,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * useFreeCreditsInit Hook
3
+ *
4
+ * Handles free credits initialization for newly registered users.
5
+ * Separated from useCredits for better maintainability and testability.
6
+ */
7
+
8
+ import { useState, useEffect, useCallback } from "react";
9
+ import {
10
+ getCreditsRepository,
11
+ getCreditsConfig,
12
+ isCreditsRepositoryConfigured,
13
+ } from "../../infrastructure/repositories/CreditsRepositoryProvider";
14
+
15
+ declare const __DEV__: boolean;
16
+
17
+ const freeCreditsInitAttempted = new Set<string>();
18
+
19
+ export interface UseFreeCreditsInitParams {
20
+ userId: string | null | undefined;
21
+ isRegisteredUser: boolean;
22
+ isAnonymous: boolean;
23
+ hasCredits: boolean;
24
+ querySuccess: boolean;
25
+ onInitComplete: () => void;
26
+ }
27
+
28
+ export interface UseFreeCreditsInitResult {
29
+ isInitializing: boolean;
30
+ needsInit: boolean;
31
+ }
32
+
33
+ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCreditsInitResult {
34
+ const { userId, isRegisteredUser, isAnonymous, hasCredits, querySuccess, onInitComplete } = params;
35
+ const [isInitializing, setIsInitializing] = useState(false);
36
+
37
+ const isConfigured = isCreditsRepositoryConfigured();
38
+ const config = getCreditsConfig();
39
+ const freeCredits = config.freeCredits ?? 0;
40
+ const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
41
+
42
+ const needsInit =
43
+ querySuccess &&
44
+ !!userId &&
45
+ isRegisteredUser &&
46
+ isConfigured &&
47
+ !hasCredits &&
48
+ autoInit &&
49
+ !freeCreditsInitAttempted.has(userId);
50
+
51
+ const initializeFreeCredits = useCallback(async (uid: string) => {
52
+ freeCreditsInitAttempted.add(uid);
53
+ setIsInitializing(true);
54
+
55
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
56
+ console.log("[useFreeCreditsInit] Initializing free credits:", uid.slice(0, 8));
57
+ }
58
+
59
+ const repository = getCreditsRepository();
60
+ const result = await repository.initializeFreeCredits(uid);
61
+ setIsInitializing(false);
62
+
63
+ if (result.success) {
64
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
65
+ console.log("[useFreeCreditsInit] Free credits initialized:", result.data?.credits);
66
+ }
67
+ onInitComplete();
68
+ } else if (typeof __DEV__ !== "undefined" && __DEV__) {
69
+ console.warn("[useFreeCreditsInit] Free credits init failed:", result.error?.message);
70
+ }
71
+ }, [onInitComplete]);
72
+
73
+ useEffect(() => {
74
+ if (needsInit && userId) {
75
+ initializeFreeCredits(userId);
76
+ } else if (querySuccess && userId && isAnonymous && !hasCredits && autoInit) {
77
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
78
+ console.log("[useFreeCreditsInit] Skipping - anonymous user must register first");
79
+ }
80
+ }
81
+ }, [needsInit, userId, querySuccess, isAnonymous, hasCredits, autoInit, initializeFreeCredits]);
82
+
83
+ return {
84
+ isInitializing,
85
+ needsInit,
86
+ };
87
+ }