@umituz/react-native-subscription 2.43.5 → 2.43.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.43.5",
3
+ "version": "2.43.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,7 +1,6 @@
1
1
  import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
2
2
  import type { DeductCreditsResult } from "../core/Credits";
3
3
  import { CREDIT_ERROR_CODES, MAX_SINGLE_DEDUCTION } from "../core/CreditsConstants";
4
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
4
 
6
5
  export async function deductCreditsOperation(
7
6
  _db: Firestore,
@@ -65,8 +64,6 @@ export async function deductCreditsOperation(
65
64
 
66
65
  if (__DEV__) console.log('[DeductCreditsCommand] transaction SUCCESS, remaining:', remaining);
67
66
 
68
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
69
-
70
67
  return {
71
68
  success: true,
72
69
  remainingCredits: remaining,
@@ -1,7 +1,6 @@
1
1
  import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
2
2
  import type { DeductCreditsResult } from "../core/Credits";
3
3
  import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
4
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
4
 
6
5
  export async function refundCreditsOperation(
7
6
  _db: Firestore,
@@ -53,8 +52,6 @@ export async function refundCreditsOperation(
53
52
  return updated;
54
53
  });
55
54
 
56
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
57
-
58
55
  return {
59
56
  success: true,
60
57
  remainingCredits: remaining,
@@ -72,7 +72,7 @@ export function createDeductCreditMutationConfig(
72
72
  );
73
73
  }
74
74
  },
75
- // onSuccess removed - CREDITS_UPDATED event handles invalidation
76
- // Optimistic update already applied, event will trigger refetch if needed
75
+ // onSuccess removed - real-time sync (onSnapshot) handles automatic updates
76
+ // Optimistic update already applied, real-time listener will confirm actual value
77
77
  };
78
78
  }
@@ -56,7 +56,7 @@ export const useDeductCredit = ({
56
56
  try {
57
57
  const result = await repository.refundCredit(userId, amount);
58
58
  if (result.success) {
59
- // CREDITS_UPDATED event emitted by RefundCreditsCommand handles invalidation
59
+ // Real-time sync (onSnapshot) handles automatic update
60
60
  return true;
61
61
  }
62
62
  return false;
@@ -70,7 +70,7 @@ export const useDeductCredit = ({
70
70
  }
71
71
  return false;
72
72
  }
73
- }, [userId, repository, queryClient]);
73
+ }, [userId, repository]);
74
74
 
75
75
  return {
76
76
  checkCredits,
@@ -1,27 +1,22 @@
1
- import { useQuery, useQueryClient } from "@umituz/react-native-design-system/tanstack";
2
- import { useCallback, useMemo, useEffect } from "react";
1
+ import { useCallback, useMemo } from "react";
3
2
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
- import { SHORT_CACHE_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
6
- import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
7
3
  import {
8
- getCreditsRepository,
9
4
  getCreditsConfig,
10
5
  isCreditsRepositoryConfigured,
11
6
  } from "../infrastructure/CreditsRepositoryManager";
12
7
  import { calculateSafePercentage, canAffordAmount } from "../utils/creditValidation";
13
8
  import { isAuthenticated } from "../../subscription/utils/authGuards";
14
- import { creditsQueryKeys } from "./creditsQueryKeys";
15
9
  import type { UseCreditsResult, CreditsLoadStatus } from "./useCredits.types";
16
- import type { UserCredits } from "../core/Credits";
10
+ import { useCreditsRealTime } from "./useCreditsRealTime";
17
11
 
18
12
  const deriveLoadStatus = (
19
- queryStatus: "pending" | "error" | "success",
13
+ isLoading: boolean,
14
+ error: Error | null,
20
15
  queryEnabled: boolean
21
16
  ): CreditsLoadStatus => {
22
17
  if (!queryEnabled) return "idle";
23
- if (queryStatus === "pending") return "loading";
24
- if (queryStatus === "error") return "error";
18
+ if (isLoading) return "loading";
19
+ if (error) return "error";
25
20
  return "ready";
26
21
  };
27
22
 
@@ -33,41 +28,7 @@ export const useCredits = (): UseCreditsResult => {
33
28
  const hasUser = isAuthenticated(userId);
34
29
  const queryEnabled = hasUser && isConfigured;
35
30
 
36
- const { data, status, error, refetch } = useQuery<UserCredits | null, Error>({
37
- queryKey: creditsQueryKeys.user(userId),
38
- queryFn: async () => {
39
- if (!hasUser || !isConfigured) return null;
40
-
41
- const repository = getCreditsRepository();
42
- const result = await repository.getCredits(userId);
43
-
44
- if (!result.success) {
45
- throw new Error(result.error?.message || "Failed to fetch credits");
46
- }
47
-
48
- return result.data ?? null;
49
- },
50
- enabled: queryEnabled,
51
- ...SHORT_CACHE_CONFIG,
52
- });
53
-
54
- const queryClient = useQueryClient();
55
-
56
- usePreviousUserCleanup(userId, queryClient, creditsQueryKeys.user);
57
-
58
- useEffect(() => {
59
- if (!hasUser) return undefined;
60
-
61
- const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
62
- if (updatedUserId === userId) {
63
- queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
64
- }
65
- });
66
-
67
- return unsubscribe;
68
- }, [userId, hasUser, queryClient]);
69
-
70
- const credits = data ?? null;
31
+ const { credits, isLoading, error } = useCreditsRealTime(userId);
71
32
 
72
33
  const derivedValues = useMemo(() => {
73
34
  const has = (credits?.credits ?? 0) > 0;
@@ -81,19 +42,18 @@ export const useCredits = (): UseCreditsResult => {
81
42
  [credits]
82
43
  );
83
44
 
84
- const loadStatus = deriveLoadStatus(status, queryEnabled);
45
+ const loadStatus = deriveLoadStatus(isLoading, error, queryEnabled);
85
46
  const isCreditsLoaded = loadStatus === "ready";
86
- const isLoading = loadStatus === "loading";
87
47
 
88
48
  return {
89
49
  credits,
90
50
  isLoading,
91
51
  isCreditsLoaded,
92
52
  loadStatus,
93
- error: error instanceof Error ? error : null,
53
+ error,
94
54
  hasCredits: derivedValues.hasCredits,
95
55
  creditsPercent: derivedValues.creditsPercent,
96
- refetch,
56
+ refetch: async () => {},
97
57
  canAfford,
98
58
  };
99
59
  };
@@ -0,0 +1,115 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import { onSnapshot } from "firebase/firestore";
3
+ import type { UserCredits } from "../core/Credits";
4
+ import { getCreditsConfig } from "../infrastructure/CreditsRepositoryManager";
5
+ import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
6
+ import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore/collectionUtils";
7
+ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
8
+
9
+ /**
10
+ * Real-time sync for credits using Firestore onSnapshot.
11
+ * Provides instant updates without cache invalidation complexity.
12
+ *
13
+ * Benefits:
14
+ * - Zero cache invalidation needed
15
+ * - Instant updates from Firestore
16
+ * - Always consistent with server state
17
+ * - Simpler code (no event listeners needed)
18
+ *
19
+ * @param userId - User ID to sync credits for
20
+ * @returns Credits state and loading status
21
+ */
22
+ export function useCreditsRealTime(userId: string | null | undefined) {
23
+ const [credits, setCredits] = useState<UserCredits | null>(null);
24
+ const [isLoading, setIsLoading] = useState(true);
25
+ const [error, setError] = useState<Error | null>(null);
26
+
27
+ useEffect(() => {
28
+ // Reset state when userId changes
29
+ if (!userId) {
30
+ setCredits(null);
31
+ setIsLoading(false);
32
+ setError(null);
33
+ return;
34
+ }
35
+
36
+ setIsLoading(true);
37
+ setError(null);
38
+
39
+ try {
40
+ const db = requireFirestore();
41
+ const config = getCreditsConfig();
42
+
43
+ // Build doc ref using same logic as repository
44
+ const collectionConfig: CollectionConfig = {
45
+ collectionName: config.collectionName,
46
+ useUserSubcollection: config.useUserSubcollection,
47
+ };
48
+ const docRef = buildDocRef(db, userId, "balance", collectionConfig);
49
+
50
+ // Real-time listener
51
+ const unsubscribe = onSnapshot(
52
+ docRef,
53
+ (snapshot) => {
54
+ if (snapshot.exists()) {
55
+ const entity = mapCreditsDocumentToEntity(snapshot.data() as UserCreditsDocumentRead);
56
+ setCredits(entity);
57
+ } else {
58
+ setCredits(null);
59
+ }
60
+ setIsLoading(false);
61
+ },
62
+ (err) => {
63
+ console.error("[useCreditsRealTime] Snapshot error:", err);
64
+ setError(err as Error);
65
+ setIsLoading(false);
66
+ }
67
+ );
68
+
69
+ return () => {
70
+ unsubscribe();
71
+ };
72
+ } catch (err) {
73
+ const error = err instanceof Error ? err : new Error(String(err));
74
+ console.error("[useCreditsRealTime] Setup error:", err);
75
+ setError(error);
76
+ setIsLoading(false);
77
+ }
78
+ }, [userId]);
79
+
80
+ const refetch = useCallback(() => {
81
+ // Real-time sync doesn't need refetch, but keep for API compatibility
82
+ // The snapshot listener will automatically update when data changes
83
+ if (__DEV__) {
84
+ console.warn("[useCreditsRealTime] Refetch called - not needed for real-time sync");
85
+ }
86
+ }, []);
87
+
88
+ return {
89
+ credits,
90
+ isLoading,
91
+ error,
92
+ refetch, // No-op but kept for compatibility
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Hook to get derived credit values with real-time sync.
98
+ * This is the real-time equivalent of the computed values in useCredits.
99
+ */
100
+ export function useCreditsRealTimeDerived(userId: string | null | undefined) {
101
+ const { credits, isLoading } = useCreditsRealTime(userId);
102
+
103
+ const hasCredits = (credits?.credits ?? 0) > 0;
104
+ const creditsPercent = credits ? Math.min(
105
+ (credits.credits / credits.creditLimit) * 100,
106
+ 100
107
+ ) : 0;
108
+
109
+ return {
110
+ hasCredits,
111
+ creditsPercent,
112
+ isLoading,
113
+ credits,
114
+ };
115
+ }
@@ -234,7 +234,6 @@ export class SubscriptionSyncProcessor {
234
234
  throw new Error(`[SubscriptionSyncProcessor] Credit initialization failed for purchase: ${result.error?.message ?? 'unknown'}`);
235
235
  }
236
236
 
237
- this.emitCreditsUpdated(creditsUserId);
238
237
  if (typeof __DEV__ !== "undefined" && __DEV__) {
239
238
  console.log('[SubscriptionSyncProcessor] 🟢 processPurchase: Credits initialized successfully', {
240
239
  creditsUserId,
@@ -271,7 +270,6 @@ export class SubscriptionSyncProcessor {
271
270
  throw new Error(`[SubscriptionSyncProcessor] Credit initialization failed for renewal: ${result.error?.message ?? 'unknown'}`);
272
271
  }
273
272
 
274
- this.emitCreditsUpdated(creditsUserId);
275
273
  } finally {
276
274
  this.purchaseInProgress = false;
277
275
  }
@@ -315,7 +313,6 @@ export class SubscriptionSyncProcessor {
315
313
 
316
314
  private async expireSubscription(userId: string): Promise<void> {
317
315
  await getCreditsRepository().syncExpiredStatus(userId);
318
- this.emitCreditsUpdated(userId);
319
316
  }
320
317
 
321
318
  private async syncPremiumStatus(userId: string, event: PremiumStatusChangedEvent): Promise<void> {
@@ -357,7 +354,6 @@ export class SubscriptionSyncProcessor {
357
354
  store: event.store ?? null,
358
355
  ownershipType: event.ownershipType ?? null,
359
356
  });
360
- this.emitCreditsUpdated(userId);
361
357
 
362
358
  if (__DEV__) {
363
359
  console.log('[SubscriptionSyncProcessor] 🟢 syncPremiumStatus: Completed', {
@@ -367,8 +363,4 @@ export class SubscriptionSyncProcessor {
367
363
  });
368
364
  }
369
365
  }
370
-
371
- private emitCreditsUpdated(userId: string): void {
372
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
373
- }
374
366
  }
@@ -41,9 +41,9 @@ export const usePurchasePackage = () => {
41
41
  // Invalidate packages cache (no event listener for packages)
42
42
  queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
43
43
 
44
- // Credits and subscription status are invalidated via events:
45
- // - CREDITS_UPDATED event (SubscriptionSyncProcessor → useCredits)
46
- // - PREMIUM_STATUS_CHANGED event (SubscriptionSyncProcessor → useSubscriptionStatus)
44
+ // Credits and subscription status updated via real-time sync:
45
+ // - Credits: Firestore onSnapshot (useCreditsRealTime)
46
+ // - Subscription status: PREMIUM_STATUS_CHANGED event (RevenueCat)
47
47
  // No manual invalidation needed here
48
48
  } else {
49
49
  showError("Purchase Failed", "Unable to complete purchase. Please try again.");
@@ -32,9 +32,9 @@ export const useRestorePurchase = () => {
32
32
  // Invalidate packages cache (no event listener for packages)
33
33
  queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
34
34
 
35
- // Credits and subscription status are invalidated via events:
36
- // - CREDITS_UPDATED event (SubscriptionSyncProcessor → useCredits)
37
- // - PREMIUM_STATUS_CHANGED event (SubscriptionSyncProcessor → useSubscriptionStatus)
35
+ // Credits and subscription status updated via real-time sync:
36
+ // - Credits: Firestore onSnapshot (useCreditsRealTime)
37
+ // - Subscription status: PREMIUM_STATUS_CHANGED event (RevenueCat)
38
38
  // No manual invalidation needed here
39
39
 
40
40
  if (result.productId) {
@@ -7,7 +7,6 @@ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
7
7
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
8
8
  import { ScreenLayout } from "../../../../shared/presentation/layouts/ScreenLayout";
9
9
  import { getCreditsRepository } from "../../../credits/infrastructure/CreditsRepositoryManager";
10
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../../shared/infrastructure/SubscriptionEventBus";
11
10
  import { SubscriptionHeader } from "./components/SubscriptionHeader";
12
11
  import { CreditsList } from "./components/CreditsList";
13
12
  import { UpgradePrompt } from "./components/UpgradePrompt";
@@ -116,7 +115,6 @@ const DevTestPanel: React.FC<{ statusType: string }> = ({ statusType }) => {
116
115
  const userId = selectUserId(useAuthStore.getState());
117
116
  if (!userId) throw new Error("No userId found");
118
117
  await getCreditsRepository().syncExpiredStatus(userId);
119
- subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
120
118
  }), [run]);
121
119
 
122
120
  const handleRestore = useCallback(() => run("Restore", async () => {