@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.47",
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 (NOT free credits)
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" | "initializing" | "ready" | "error";
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
- // Include needsInit in initializing state for accurate loading detection
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" || loadStatus === "initializing";
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
- }