@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
|
+
"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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
|
|
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,
|
|
66
|
+
const { data, status, error, refetch } = useQuery({
|
|
55
67
|
queryKey: creditsQueryKeys.user(userId ?? ""),
|
|
56
68
|
queryFn: async () => {
|
|
57
|
-
if (!userId || !isConfigured)
|
|
58
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|