@umituz/react-native-subscription 2.27.7 → 2.27.9
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.9",
|
|
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",
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* useFreeCreditsInit Hook
|
|
3
3
|
*
|
|
4
4
|
* Handles free credits initialization for newly registered users.
|
|
5
|
-
*
|
|
5
|
+
* Uses singleton pattern to prevent race conditions across multiple hook instances.
|
|
6
|
+
*
|
|
7
|
+
* @see https://medium.com/@shubhamkandharkar/creating-a-singleton-hook-in-react-a-practical-guide-fe5bf9aaefed
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import { useEffect, useCallback, useSyncExternalStore } from "react";
|
|
9
11
|
import {
|
|
10
12
|
getCreditsRepository,
|
|
11
13
|
getCreditsConfig,
|
|
@@ -14,8 +16,82 @@ import {
|
|
|
14
16
|
|
|
15
17
|
declare const __DEV__: boolean;
|
|
16
18
|
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// SINGLETON STATE - Shared across all hook instances
|
|
21
|
+
// ============================================================================
|
|
17
22
|
const freeCreditsInitAttempted = new Set<string>();
|
|
23
|
+
const freeCreditsInitInProgress = new Set<string>();
|
|
24
|
+
const initPromises = new Map<string, Promise<boolean>>();
|
|
25
|
+
const subscribers = new Set<() => void>();
|
|
26
|
+
|
|
27
|
+
function notifySubscribers(): void {
|
|
28
|
+
subscribers.forEach((cb) => cb());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function subscribe(callback: () => void): () => void {
|
|
32
|
+
subscribers.add(callback);
|
|
33
|
+
return () => subscribers.delete(callback);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getSnapshot(): Set<string> {
|
|
37
|
+
return freeCreditsInitInProgress;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function initializeFreeCreditsForUser(
|
|
41
|
+
userId: string,
|
|
42
|
+
onComplete: () => void
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
// Already completed for this user
|
|
45
|
+
if (freeCreditsInitAttempted.has(userId) && !freeCreditsInitInProgress.has(userId)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Already in progress - return existing promise
|
|
50
|
+
const existingPromise = initPromises.get(userId);
|
|
51
|
+
if (existingPromise) {
|
|
52
|
+
return existingPromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Mark as attempted and in progress
|
|
56
|
+
freeCreditsInitAttempted.add(userId);
|
|
57
|
+
freeCreditsInitInProgress.add(userId);
|
|
58
|
+
notifySubscribers();
|
|
59
|
+
|
|
60
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
+
console.log("[useFreeCreditsInit] Initializing free credits:", userId.slice(0, 8));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const promise = (async () => {
|
|
65
|
+
try {
|
|
66
|
+
const repository = getCreditsRepository();
|
|
67
|
+
const result = await repository.initializeFreeCredits(userId);
|
|
68
|
+
|
|
69
|
+
if (result.success) {
|
|
70
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
71
|
+
console.log("[useFreeCreditsInit] Free credits initialized:", result.data?.credits);
|
|
72
|
+
}
|
|
73
|
+
onComplete();
|
|
74
|
+
return true;
|
|
75
|
+
} else {
|
|
76
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
77
|
+
console.warn("[useFreeCreditsInit] Free credits init failed:", result.error?.message);
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
freeCreditsInitInProgress.delete(userId);
|
|
83
|
+
initPromises.delete(userId);
|
|
84
|
+
notifySubscribers();
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
initPromises.set(userId, promise);
|
|
89
|
+
return promise;
|
|
90
|
+
}
|
|
18
91
|
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// HOOK INTERFACE
|
|
94
|
+
// ============================================================================
|
|
19
95
|
export interface UseFreeCreditsInitParams {
|
|
20
96
|
userId: string | null | undefined;
|
|
21
97
|
isRegisteredUser: boolean;
|
|
@@ -32,13 +108,19 @@ export interface UseFreeCreditsInitResult {
|
|
|
32
108
|
|
|
33
109
|
export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCreditsInitResult {
|
|
34
110
|
const { userId, isRegisteredUser, isAnonymous, hasCredits, querySuccess, onInitComplete } = params;
|
|
35
|
-
|
|
111
|
+
|
|
112
|
+
// Subscribe to singleton state changes
|
|
113
|
+
const inProgressSet = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
36
114
|
|
|
37
115
|
const isConfigured = isCreditsRepositoryConfigured();
|
|
38
116
|
const config = getCreditsConfig();
|
|
39
117
|
const freeCredits = config.freeCredits ?? 0;
|
|
40
118
|
const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
|
|
41
119
|
|
|
120
|
+
// Check if THIS user's init is in progress (shared across all hook instances)
|
|
121
|
+
const isInitializing = userId ? inProgressSet.has(userId) : false;
|
|
122
|
+
|
|
123
|
+
// Need init if: query succeeded, registered user, no credits, not attempted yet
|
|
42
124
|
const needsInit =
|
|
43
125
|
querySuccess &&
|
|
44
126
|
!!userId &&
|
|
@@ -48,40 +130,28 @@ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCre
|
|
|
48
130
|
autoInit &&
|
|
49
131
|
!freeCreditsInitAttempted.has(userId);
|
|
50
132
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
}
|
|
133
|
+
// Stable callback reference
|
|
134
|
+
const stableOnComplete = useCallback(() => {
|
|
135
|
+
onInitComplete();
|
|
71
136
|
}, [onInitComplete]);
|
|
72
137
|
|
|
73
138
|
useEffect(() => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
139
|
+
if (!userId) return;
|
|
140
|
+
|
|
141
|
+
if (needsInit) {
|
|
142
|
+
// Double-check inside effect to handle race conditions
|
|
143
|
+
if (!freeCreditsInitAttempted.has(userId)) {
|
|
144
|
+
initializeFreeCreditsForUser(userId, stableOnComplete);
|
|
145
|
+
}
|
|
146
|
+
} else if (querySuccess && isAnonymous && !hasCredits && autoInit) {
|
|
77
147
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
148
|
console.log("[useFreeCreditsInit] Skipping - anonymous user must register first");
|
|
79
149
|
}
|
|
80
150
|
}
|
|
81
|
-
}, [needsInit, userId, querySuccess, isAnonymous, hasCredits, autoInit,
|
|
151
|
+
}, [needsInit, userId, querySuccess, isAnonymous, hasCredits, autoInit, stableOnComplete]);
|
|
82
152
|
|
|
83
153
|
return {
|
|
84
|
-
isInitializing,
|
|
154
|
+
isInitializing: isInitializing || needsInit,
|
|
85
155
|
needsInit,
|
|
86
156
|
};
|
|
87
157
|
}
|