@umituz/react-native-subscription 2.27.47 → 2.27.49
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 +1 -1
- package/src/domain/entities/Credits.ts +0 -6
- package/src/infrastructure/repositories/CreditsRepository.ts +0 -5
- package/src/infrastructure/services/CreditsInitializer.ts +1 -2
- package/src/presentation/hooks/useCredits.ts +4 -32
- package/src/infrastructure/services/FreeCreditsService.ts +0 -83
- package/src/presentation/hooks/useFreeCreditsInit.ts +0 -174
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.49",
|
|
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",
|
|
@@ -74,10 +74,6 @@ export interface CreditsConfig {
|
|
|
74
74
|
creditPackageAmounts?: Record<string, number>;
|
|
75
75
|
/** Credit allocations for different subscription types (weekly, monthly, yearly) */
|
|
76
76
|
packageAllocations?: PackageAllocationMap;
|
|
77
|
-
/** Enable free credits for new users (default: false) */
|
|
78
|
-
enableFreeCredits?: boolean;
|
|
79
|
-
/** Free credits given to new users on registration (only used when enableFreeCredits: true) */
|
|
80
|
-
freeCredits?: number;
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
export interface CreditsResult<T = UserCredits> {
|
|
@@ -101,6 +97,4 @@ export interface DeductCreditsResult {
|
|
|
101
97
|
export const DEFAULT_CREDITS_CONFIG: CreditsConfig = {
|
|
102
98
|
collectionName: "user_credits",
|
|
103
99
|
creditLimit: 100,
|
|
104
|
-
enableFreeCredits: false,
|
|
105
|
-
freeCredits: 0,
|
|
106
100
|
};
|
|
@@ -12,7 +12,6 @@ import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
|
12
12
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
13
13
|
import { CreditsMapper } from "../mappers/CreditsMapper";
|
|
14
14
|
import type { RevenueCatData } from "../../domain/types/RevenueCatData";
|
|
15
|
-
import { initializeFreeCredits as initializeFreeCreditsService } from "../services/FreeCreditsService";
|
|
16
15
|
|
|
17
16
|
export type { RevenueCatData } from "../../domain/types/RevenueCatData";
|
|
18
17
|
|
|
@@ -111,10 +110,6 @@ export class CreditsRepository extends BaseRepository {
|
|
|
111
110
|
return !!(res.success && res.data && res.data.credits >= cost);
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
async initializeFreeCredits(userId: string): Promise<CreditsResult> {
|
|
115
|
-
return initializeFreeCreditsService({ config: this.config, getRef: this.getRef.bind(this) }, userId);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
113
|
async syncExpiredStatus(userId: string): Promise<void> {
|
|
119
114
|
const db = getFirestore();
|
|
120
115
|
if (!db) return;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credits Initializer
|
|
3
|
-
* Handles subscription credit initialization
|
|
4
|
-
* Free credits are handled by CreditsRepository.initializeFreeCredits()
|
|
3
|
+
* Handles subscription credit initialization for premium users
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { Platform } from "react-native";
|
|
@@ -3,23 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fetches user credits with TanStack Query best practices.
|
|
5
5
|
* Uses status-based state management for reliable loading detection.
|
|
6
|
-
* Free credits initialization is delegated to useFreeCreditsInit hook.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
10
9
|
import { useCallback, useMemo } from "react";
|
|
11
|
-
import {
|
|
12
|
-
useAuthStore,
|
|
13
|
-
selectUserId,
|
|
14
|
-
selectIsAnonymous,
|
|
15
|
-
} from "@umituz/react-native-auth";
|
|
10
|
+
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
16
11
|
import type { UserCredits } from "../../domain/entities/Credits";
|
|
17
12
|
import {
|
|
18
13
|
getCreditsRepository,
|
|
19
14
|
getCreditsConfig,
|
|
20
15
|
isCreditsRepositoryConfigured,
|
|
21
16
|
} from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
22
|
-
import { useFreeCreditsInit } from "./useFreeCreditsInit";
|
|
23
17
|
|
|
24
18
|
declare const __DEV__: boolean;
|
|
25
19
|
|
|
@@ -28,7 +22,7 @@ export const creditsQueryKeys = {
|
|
|
28
22
|
user: (userId: string) => ["credits", userId] as const,
|
|
29
23
|
};
|
|
30
24
|
|
|
31
|
-
export type CreditsLoadStatus = "idle" | "loading" | "
|
|
25
|
+
export type CreditsLoadStatus = "idle" | "loading" | "ready" | "error";
|
|
32
26
|
|
|
33
27
|
export interface UseCreditsResult {
|
|
34
28
|
credits: UserCredits | null;
|
|
@@ -44,21 +38,16 @@ export interface UseCreditsResult {
|
|
|
44
38
|
|
|
45
39
|
function deriveLoadStatus(
|
|
46
40
|
queryStatus: "pending" | "error" | "success",
|
|
47
|
-
isInitializing: boolean,
|
|
48
41
|
queryEnabled: boolean
|
|
49
42
|
): CreditsLoadStatus {
|
|
50
43
|
if (!queryEnabled) return "idle";
|
|
51
44
|
if (queryStatus === "pending") return "loading";
|
|
52
45
|
if (queryStatus === "error") return "error";
|
|
53
|
-
if (isInitializing) return "initializing";
|
|
54
46
|
return "ready";
|
|
55
47
|
}
|
|
56
48
|
|
|
57
49
|
export const useCredits = (): UseCreditsResult => {
|
|
58
50
|
const userId = useAuthStore(selectUserId);
|
|
59
|
-
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
60
|
-
const isRegisteredUser = !!userId && !isAnonymous;
|
|
61
|
-
|
|
62
51
|
const isConfigured = isCreditsRepositoryConfigured();
|
|
63
52
|
const config = getCreditsConfig();
|
|
64
53
|
const queryEnabled = !!userId && isConfigured;
|
|
@@ -104,18 +93,6 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
104
93
|
});
|
|
105
94
|
|
|
106
95
|
const credits = data ?? null;
|
|
107
|
-
const querySuccess = status === "success";
|
|
108
|
-
const hasCreditsData = (credits?.credits ?? 0) > 0;
|
|
109
|
-
|
|
110
|
-
// Delegate free credits initialization to dedicated hook
|
|
111
|
-
const { isInitializing, needsInit } = useFreeCreditsInit({
|
|
112
|
-
userId,
|
|
113
|
-
isRegisteredUser,
|
|
114
|
-
isAnonymous,
|
|
115
|
-
hasCredits: hasCreditsData,
|
|
116
|
-
querySuccess,
|
|
117
|
-
onInitComplete: refetch,
|
|
118
|
-
});
|
|
119
96
|
|
|
120
97
|
const derivedValues = useMemo(() => {
|
|
121
98
|
const has = (credits?.credits ?? 0) > 0;
|
|
@@ -128,14 +105,9 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
128
105
|
[credits]
|
|
129
106
|
);
|
|
130
107
|
|
|
131
|
-
|
|
132
|
-
const loadStatus = deriveLoadStatus(
|
|
133
|
-
status,
|
|
134
|
-
isInitializing || needsInit,
|
|
135
|
-
queryEnabled
|
|
136
|
-
);
|
|
108
|
+
const loadStatus = deriveLoadStatus(status, queryEnabled);
|
|
137
109
|
const isCreditsLoaded = loadStatus === "ready";
|
|
138
|
-
const isLoading = loadStatus === "loading"
|
|
110
|
+
const isLoading = loadStatus === "loading";
|
|
139
111
|
|
|
140
112
|
return {
|
|
141
113
|
credits,
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Free Credits Service
|
|
3
|
-
* Handles initialization of free credits for new users
|
|
4
|
-
*/
|
|
5
|
-
declare const __DEV__: boolean;
|
|
6
|
-
|
|
7
|
-
import { runTransaction, serverTimestamp, type Firestore, type DocumentReference, type Transaction } from "firebase/firestore";
|
|
8
|
-
import { getFirestore } from "@umituz/react-native-firebase";
|
|
9
|
-
import type { CreditsConfig, CreditsResult, UserCredits } from "../../domain/entities/Credits";
|
|
10
|
-
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
11
|
-
import { CreditsMapper } from "../mappers/CreditsMapper";
|
|
12
|
-
|
|
13
|
-
export interface FreeCreditsServiceConfig {
|
|
14
|
-
config: CreditsConfig;
|
|
15
|
-
getRef: (db: Firestore, userId: string) => DocumentReference;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Initialize free credits for new users
|
|
20
|
-
* Creates a credits document with freeCredits amount (no subscription)
|
|
21
|
-
* Uses transaction to prevent race condition with premium init
|
|
22
|
-
*/
|
|
23
|
-
export async function initializeFreeCredits(
|
|
24
|
-
deps: FreeCreditsServiceConfig,
|
|
25
|
-
userId: string
|
|
26
|
-
): Promise<CreditsResult> {
|
|
27
|
-
const db = getFirestore();
|
|
28
|
-
if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
|
|
29
|
-
|
|
30
|
-
const freeCredits = deps.config.freeCredits ?? 0;
|
|
31
|
-
if (freeCredits <= 0) {
|
|
32
|
-
return { success: false, error: { message: "Free credits not configured", code: "NO_FREE_CREDITS" } };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const ref = deps.getRef(db, userId);
|
|
37
|
-
|
|
38
|
-
const result = await runTransaction(db, async (tx: Transaction) => {
|
|
39
|
-
const snap = await tx.get(ref);
|
|
40
|
-
|
|
41
|
-
// Don't overwrite if document already exists (premium or previous init)
|
|
42
|
-
if (snap.exists()) {
|
|
43
|
-
if (__DEV__) console.log("[FreeCreditsService] Credits document exists, skipping");
|
|
44
|
-
const existing = snap.data() as UserCreditsDocumentRead;
|
|
45
|
-
return { skipped: true, data: CreditsMapper.toEntity(existing) };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Create new document with free credits
|
|
49
|
-
const now = serverTimestamp();
|
|
50
|
-
const creditsData = {
|
|
51
|
-
isPremium: false,
|
|
52
|
-
status: "free" as const,
|
|
53
|
-
credits: freeCredits,
|
|
54
|
-
creditLimit: freeCredits,
|
|
55
|
-
initialFreeCredits: freeCredits,
|
|
56
|
-
isFreeCredits: true,
|
|
57
|
-
createdAt: now,
|
|
58
|
-
lastUpdatedAt: now,
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
tx.set(ref, creditsData);
|
|
62
|
-
|
|
63
|
-
if (__DEV__) console.log("[FreeCreditsService] Initialized:", { userId: userId.slice(0, 8), credits: freeCredits });
|
|
64
|
-
|
|
65
|
-
const entity: UserCredits = {
|
|
66
|
-
isPremium: false,
|
|
67
|
-
status: "free",
|
|
68
|
-
credits: freeCredits,
|
|
69
|
-
creditLimit: freeCredits,
|
|
70
|
-
purchasedAt: null,
|
|
71
|
-
expirationDate: null,
|
|
72
|
-
lastUpdatedAt: null,
|
|
73
|
-
willRenew: false,
|
|
74
|
-
};
|
|
75
|
-
return { skipped: false, data: entity };
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
return { success: true, data: result.data };
|
|
79
|
-
} catch (e: any) {
|
|
80
|
-
if (__DEV__) console.error("[FreeCreditsService] Init error:", e.message);
|
|
81
|
-
return { success: false, error: { message: e.message, code: "INIT_ERR" } };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useFreeCreditsInit Hook
|
|
3
|
-
*
|
|
4
|
-
* Handles free credits initialization for newly registered users.
|
|
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
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { useEffect, useCallback, useSyncExternalStore } from "react";
|
|
11
|
-
import {
|
|
12
|
-
getCreditsRepository,
|
|
13
|
-
getCreditsConfig,
|
|
14
|
-
isCreditsRepositoryConfigured,
|
|
15
|
-
} from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
16
|
-
|
|
17
|
-
declare const __DEV__: boolean;
|
|
18
|
-
|
|
19
|
-
// ============================================================================
|
|
20
|
-
// SINGLETON STATE - Shared across all hook instances
|
|
21
|
-
// ============================================================================
|
|
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
|
-
if (!isCreditsRepositoryConfigured()) {
|
|
67
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
|
-
console.warn("[useFreeCreditsInit] Credits repository not configured");
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const repository = getCreditsRepository();
|
|
74
|
-
const result = await repository.initializeFreeCredits(userId);
|
|
75
|
-
|
|
76
|
-
if (result.success) {
|
|
77
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
-
console.log("[useFreeCreditsInit] Free credits initialized:", result.data?.credits);
|
|
79
|
-
}
|
|
80
|
-
onComplete();
|
|
81
|
-
return true;
|
|
82
|
-
} else {
|
|
83
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
-
console.warn("[useFreeCreditsInit] Free credits init failed:", result.error?.message);
|
|
85
|
-
}
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
90
|
-
console.error("[useFreeCreditsInit] Unexpected error:", error);
|
|
91
|
-
}
|
|
92
|
-
return false;
|
|
93
|
-
} finally {
|
|
94
|
-
freeCreditsInitInProgress.delete(userId);
|
|
95
|
-
initPromises.delete(userId);
|
|
96
|
-
notifySubscribers();
|
|
97
|
-
}
|
|
98
|
-
})();
|
|
99
|
-
|
|
100
|
-
initPromises.set(userId, promise);
|
|
101
|
-
return promise;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// HOOK INTERFACE
|
|
106
|
-
// ============================================================================
|
|
107
|
-
export interface UseFreeCreditsInitParams {
|
|
108
|
-
userId: string | null | undefined;
|
|
109
|
-
isRegisteredUser: boolean;
|
|
110
|
-
isAnonymous: boolean;
|
|
111
|
-
hasCredits: boolean;
|
|
112
|
-
querySuccess: boolean;
|
|
113
|
-
onInitComplete: () => void;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface UseFreeCreditsInitResult {
|
|
117
|
-
isInitializing: boolean;
|
|
118
|
-
needsInit: boolean;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCreditsInitResult {
|
|
122
|
-
const { userId, isRegisteredUser, isAnonymous, hasCredits, querySuccess, onInitComplete } = params;
|
|
123
|
-
|
|
124
|
-
// Subscribe to singleton state changes
|
|
125
|
-
const inProgressSet = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
126
|
-
|
|
127
|
-
const isConfigured = isCreditsRepositoryConfigured();
|
|
128
|
-
const config = getCreditsConfig();
|
|
129
|
-
const freeCredits = config.freeCredits ?? 0;
|
|
130
|
-
// Free credits only enabled when explicitly set to true AND freeCredits > 0
|
|
131
|
-
const isFreeCreditsEnabled = config.enableFreeCredits === true && freeCredits > 0;
|
|
132
|
-
|
|
133
|
-
// Check if THIS user's init is in progress (shared across all hook instances)
|
|
134
|
-
const isInitializing = userId ? inProgressSet.has(userId) : false;
|
|
135
|
-
|
|
136
|
-
// Need init if: query succeeded, registered user, no credits, not attempted yet
|
|
137
|
-
const needsInit =
|
|
138
|
-
querySuccess &&
|
|
139
|
-
!!userId &&
|
|
140
|
-
isRegisteredUser &&
|
|
141
|
-
isConfigured &&
|
|
142
|
-
!hasCredits &&
|
|
143
|
-
isFreeCreditsEnabled &&
|
|
144
|
-
!freeCreditsInitAttempted.has(userId);
|
|
145
|
-
|
|
146
|
-
// Stable callback reference
|
|
147
|
-
const stableOnComplete = useCallback(() => {
|
|
148
|
-
onInitComplete();
|
|
149
|
-
}, [onInitComplete]);
|
|
150
|
-
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if (!userId) return;
|
|
153
|
-
|
|
154
|
-
if (needsInit) {
|
|
155
|
-
// Double-check inside effect to handle race conditions
|
|
156
|
-
if (!freeCreditsInitAttempted.has(userId)) {
|
|
157
|
-
initializeFreeCreditsForUser(userId, stableOnComplete).catch((error) => {
|
|
158
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
159
|
-
console.error("[useFreeCreditsInit] Init failed:", error);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
} else if (querySuccess && isAnonymous && !hasCredits && isFreeCreditsEnabled) {
|
|
164
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
165
|
-
console.log("[useFreeCreditsInit] Skipping - anonymous user must register first");
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}, [needsInit, userId, querySuccess, isAnonymous, hasCredits, isFreeCreditsEnabled, stableOnComplete]);
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
isInitializing: isInitializing || needsInit,
|
|
172
|
-
needsInit,
|
|
173
|
-
};
|
|
174
|
-
}
|