@umituz/react-native-subscription 2.43.6 → 2.43.7

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.6",
3
+ "version": "2.43.7",
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,4 +1,4 @@
1
- import { useMutation, useQueryClient } from "@umituz/react-native-design-system/tanstack";
1
+ import { useMutation } from "@umituz/react-native-design-system/tanstack";
2
2
  import type { PurchasesPackage } from "react-native-purchases";
3
3
  import { useAlert } from "@umituz/react-native-design-system/molecules";
4
4
  import {
@@ -6,7 +6,6 @@ import {
6
6
  selectUserId,
7
7
  } from "@umituz/react-native-auth";
8
8
  import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
9
- import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
10
9
  import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
11
10
 
12
11
  interface PurchaseMutationResult {
@@ -16,7 +15,6 @@ interface PurchaseMutationResult {
16
15
 
17
16
  export const usePurchasePackage = () => {
18
17
  const userId = useAuthStore(selectUserId);
19
- const queryClient = useQueryClient();
20
18
  const { showSuccess, showError } = useAlert();
21
19
 
22
20
  return useMutation({
@@ -37,14 +35,6 @@ export const usePurchasePackage = () => {
37
35
  onSuccess: (result) => {
38
36
  if (result.success) {
39
37
  showSuccess("Purchase Successful", "Your subscription is now active!");
40
-
41
- // Invalidate packages cache (no event listener for packages)
42
- queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
43
-
44
- // Credits and subscription status updated via real-time sync:
45
- // - Credits: Firestore onSnapshot (useCreditsRealTime)
46
- // - Subscription status: PREMIUM_STATUS_CHANGED event (RevenueCat)
47
- // No manual invalidation needed here
48
38
  } else {
49
39
  showError("Purchase Failed", "Unable to complete purchase. Please try again.");
50
40
  }
@@ -1,11 +1,10 @@
1
- import { useMutation, useQueryClient } from "@umituz/react-native-design-system/tanstack";
1
+ import { useMutation } from "@umituz/react-native-design-system/tanstack";
2
2
  import { useAlert } from "@umituz/react-native-design-system/molecules";
3
3
  import {
4
4
  useAuthStore,
5
5
  selectUserId,
6
6
  } from "@umituz/react-native-auth";
7
7
  import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
8
- import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
9
8
  import { getErrorMessage } from "../../../revenuecat/core/errors/RevenueCatErrorHandler";
10
9
 
11
10
  interface RestoreResult {
@@ -15,7 +14,6 @@ interface RestoreResult {
15
14
 
16
15
  export const useRestorePurchase = () => {
17
16
  const userId = useAuthStore(selectUserId);
18
- const queryClient = useQueryClient();
19
17
  const { showSuccess, showInfo, showError } = useAlert();
20
18
 
21
19
  return useMutation({
@@ -29,14 +27,6 @@ export const useRestorePurchase = () => {
29
27
  },
30
28
  onSuccess: (result) => {
31
29
  if (result.success) {
32
- // Invalidate packages cache (no event listener for packages)
33
- queryClient.invalidateQueries({ queryKey: SUBSCRIPTION_QUERY_KEYS.packages });
34
-
35
- // Credits and subscription status updated via real-time sync:
36
- // - Credits: Firestore onSnapshot (useCreditsRealTime)
37
- // - Subscription status: PREMIUM_STATUS_CHANGED event (RevenueCat)
38
- // No manual invalidation needed here
39
-
40
30
  if (result.productId) {
41
31
  showSuccess("Restore Successful", "Your subscription has been restored!");
42
32
  } else {
@@ -1,21 +1,20 @@
1
- import { useQuery, useQueryClient } from "@umituz/react-native-design-system/tanstack";
2
- import { useEffect, useRef, useSyncExternalStore } from "react";
1
+ import { useState, useEffect, useSyncExternalStore, useRef, useCallback } from "react";
3
2
  import {
4
3
  useAuthStore,
5
4
  selectUserId,
6
5
  } from "@umituz/react-native-auth";
7
6
  import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
8
7
  import { initializationState } from "../../infrastructure/state/initializationState";
9
- import {
10
- SUBSCRIPTION_QUERY_KEYS,
11
- } from "./subscriptionQueryKeys";
12
8
 
13
9
  export const useSubscriptionPackages = () => {
14
10
  const userId = useAuthStore(selectUserId);
15
11
  const isConfigured = SubscriptionManager.isConfigured();
16
- const queryClient = useQueryClient();
17
12
  const prevUserIdRef = useRef(userId);
18
13
 
14
+ const [packages, setPackages] = useState<any[] | null>(null);
15
+ const [isLoading, setIsLoading] = useState(true);
16
+ const [error, setError] = useState<Error | null>(null);
17
+
19
18
  const initState = useSyncExternalStore(
20
19
  initializationState.subscribe,
21
20
  initializationState.getSnapshot,
@@ -24,45 +23,48 @@ export const useSubscriptionPackages = () => {
24
23
 
25
24
  const isInitialized = initState.initialized || SubscriptionManager.isInitialized();
26
25
 
27
- const query = useQuery({
28
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId ?? "anonymous"] as const,
29
- queryFn: async () => {
30
- return SubscriptionManager.getPackages();
31
- },
32
- enabled: isConfigured && isInitialized,
33
- gcTime: 5 * 60 * 1000,
34
- staleTime: 2 * 60 * 1000,
35
- refetchOnMount: true,
36
- refetchOnWindowFocus: false,
37
- refetchOnReconnect: true,
38
- });
26
+ const fetchPackages = useCallback(async () => {
27
+ if (!isConfigured || !isInitialized) {
28
+ setPackages(null);
29
+ setIsLoading(false);
30
+ setError(null);
31
+ return;
32
+ }
33
+
34
+ setIsLoading(true);
35
+ setError(null);
36
+
37
+ try {
38
+ const result = await SubscriptionManager.getPackages();
39
+ setPackages(result);
40
+ } catch (err) {
41
+ setError(err as Error);
42
+ } finally {
43
+ setIsLoading(false);
44
+ }
45
+ }, [isConfigured, isInitialized]);
46
+
47
+ useEffect(() => {
48
+ fetchPackages();
49
+ }, [fetchPackages]);
39
50
 
40
51
  useEffect(() => {
41
52
  const prevUserId = prevUserIdRef.current;
42
53
  prevUserIdRef.current = userId;
43
54
 
44
55
  if (prevUserId !== userId) {
45
- // Clean up previous user's cache to prevent data leakage
46
- if (prevUserId) {
47
- queryClient.cancelQueries({
48
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
49
- });
50
- queryClient.removeQueries({
51
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
52
- });
53
- } else {
54
- queryClient.cancelQueries({
55
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
56
- });
57
- queryClient.removeQueries({
58
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
59
- });
60
- }
61
-
62
- // No need to invalidate - removeQueries already cleared cache
63
- // Query will refetch automatically on mount if needed
56
+ fetchPackages();
64
57
  }
65
- }, [userId, queryClient]);
58
+ }, [userId, fetchPackages]);
59
+
60
+ const refetch = useCallback(() => {
61
+ fetchPackages();
62
+ }, [fetchPackages]);
66
63
 
67
- return query;
64
+ return {
65
+ data: packages,
66
+ isLoading,
67
+ error,
68
+ refetch,
69
+ };
68
70
  };
@@ -1,26 +1,21 @@
1
- import { useQuery, useQueryClient } from "@umituz/react-native-design-system/tanstack";
2
- import { useEffect, useSyncExternalStore } from "react";
1
+ import { useState, useEffect, useSyncExternalStore, useCallback } from "react";
3
2
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
3
  import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
5
4
  import { initializationState } from "../infrastructure/state/initializationState";
6
5
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
7
- import { SubscriptionStatusResult } from "./useSubscriptionStatus.types";
6
+ import type { SubscriptionStatusResult } from "./useSubscriptionStatus.types";
7
+ import type { PremiumStatus } from "../core/types/PremiumStatus";
8
8
  import { isAuthenticated } from "../utils/authGuards";
9
- import { SHORT_CACHE_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
10
- import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
11
-
12
- export const subscriptionStatusQueryKeys = {
13
- all: ["subscriptionStatus"] as const,
14
- user: (userId: string | null | undefined) =>
15
- userId ? (["subscriptionStatus", userId] as const) : (["subscriptionStatus"] as const),
16
- };
17
9
 
18
10
  export const useSubscriptionStatus = (): SubscriptionStatusResult => {
19
11
  const userId = useAuthStore(selectUserId);
20
- const queryClient = useQueryClient();
21
12
  const isConfigured = SubscriptionManager.isConfigured();
22
13
  const hasUser = isAuthenticated(userId);
23
14
 
15
+ const [data, setData] = useState<PremiumStatus | null>(null);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [error, setError] = useState<Error | null>(null);
18
+
24
19
  const initState = useSyncExternalStore(
25
20
  initializationState.subscribe,
26
21
  initializationState.getSnapshot,
@@ -31,22 +26,30 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
31
26
  ? initState.initialized && initState.userId === userId
32
27
  : false;
33
28
 
34
- const queryEnabled = hasUser && isConfigured && isInitialized;
29
+ const fetchStatus = useCallback(async () => {
30
+ if (!hasUser || !isConfigured || !isInitialized) {
31
+ setData(null);
32
+ setIsLoading(false);
33
+ setError(null);
34
+ return;
35
+ }
35
36
 
36
- const { data, status, error, refetch } = useQuery({
37
- queryKey: subscriptionStatusQueryKeys.user(userId),
38
- queryFn: async () => {
39
- if (!hasUser) {
40
- return null;
41
- }
37
+ setIsLoading(true);
38
+ setError(null);
42
39
 
43
- return SubscriptionManager.checkPremiumStatus();
44
- },
45
- enabled: queryEnabled,
46
- ...SHORT_CACHE_CONFIG,
47
- });
40
+ try {
41
+ const result = await SubscriptionManager.checkPremiumStatus();
42
+ setData(result);
43
+ } catch (err) {
44
+ setError(err as Error);
45
+ } finally {
46
+ setIsLoading(false);
47
+ }
48
+ }, [hasUser, isConfigured, isInitialized]);
48
49
 
49
- usePreviousUserCleanup(userId, queryClient, subscriptionStatusQueryKeys.user);
50
+ useEffect(() => {
51
+ fetchStatus();
52
+ }, [fetchStatus]);
50
53
 
51
54
  useEffect(() => {
52
55
  if (!hasUser) return undefined;
@@ -55,34 +58,18 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
55
58
  SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
56
59
  (event: { userId: string; isPremium: boolean }) => {
57
60
  if (event.userId === userId) {
58
- queryClient.invalidateQueries({
59
- queryKey: subscriptionStatusQueryKeys.user(userId),
60
- });
61
+ fetchStatus();
61
62
  }
62
63
  }
63
64
  );
64
65
 
65
66
  return unsubscribe;
66
- }, [userId, hasUser, queryClient]);
67
-
68
- const isLoading = status === "pending";
67
+ }, [userId, hasUser, fetchStatus]);
69
68
 
70
69
  return {
71
- isPremium: data?.isPremium ?? false,
72
- expirationDate: data?.expirationDate ?? null,
73
- willRenew: data?.willRenew ?? false,
74
- productIdentifier: data?.productIdentifier ?? null,
75
- originalPurchaseDate: data?.originalPurchaseDate ?? null,
76
- latestPurchaseDate: data?.latestPurchaseDate ?? null,
77
- billingIssuesDetected: data?.billingIssuesDetected ?? false,
78
- isSandbox: data?.isSandbox ?? false,
79
- periodType: data?.periodType ?? null,
80
- packageType: data?.packageType ?? null,
81
- store: data?.store ?? null,
82
- gracePeriodExpiresDate: data?.gracePeriodExpiresDate ?? null,
83
- unsubscribeDetectedAt: data?.unsubscribeDetectedAt ?? null,
70
+ ...data,
84
71
  isLoading,
85
- error: error as Error | null,
86
- refetch,
87
- };
72
+ error,
73
+ refetch: fetchStatus,
74
+ } as SubscriptionStatusResult;
88
75
  };
@@ -1,17 +1,11 @@
1
- import { useQuery } from "@umituz/react-native-design-system/tanstack";
2
- import { useMemo } from "react";
1
+ import { useState, useEffect } from "react";
3
2
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
- import { MEDIUM_CACHE_CONFIG } from "../../../../shared/infrastructure/react-query/queryConfig";
3
+ import { collection, onSnapshot, query, orderBy, limit, Query } from "firebase/firestore";
5
4
  import type {
6
5
  CreditLog,
7
6
  TransactionRepositoryConfig,
8
7
  } from "../../domain/types/transaction.types";
9
- import { TransactionRepository } from "../../infrastructure/repositories/transaction/TransactionRepository";
10
-
11
- const transactionQueryKeys = {
12
- all: ["transactions"] as const,
13
- user: (userId: string) => ["transactions", userId] as const,
14
- };
8
+ import { requireFirestore } from "../../../../shared/infrastructure/firestore/collectionUtils";
15
9
 
16
10
  export interface UseTransactionHistoryParams {
17
11
  config: TransactionRepositoryConfig;
@@ -28,43 +22,75 @@ interface UseTransactionHistoryResult {
28
22
 
29
23
  export function useTransactionHistory({
30
24
  config,
31
- limit = 50,
25
+ limit: limitCount = 50,
32
26
  }: UseTransactionHistoryParams): UseTransactionHistoryResult {
33
27
  const userId = useAuthStore(selectUserId);
28
+ const [transactions, setTransactions] = useState<CreditLog[]>([]);
29
+ const [isLoading, setIsLoading] = useState(true);
30
+ const [error, setError] = useState<Error | null>(null);
34
31
 
35
- const repository = useMemo(
36
- () => new TransactionRepository(config),
37
- [config]
38
- );
32
+ useEffect(() => {
33
+ if (!userId) {
34
+ setTransactions([]);
35
+ setIsLoading(false);
36
+ setError(null);
37
+ return;
38
+ }
39
39
 
40
- const hasUser = !!userId;
40
+ setIsLoading(true);
41
+ setError(null);
41
42
 
42
- const { data, isLoading, error, refetch } = useQuery({
43
- queryKey: [...transactionQueryKeys.user(userId ?? ""), limit],
44
- queryFn: async () => {
45
- if (!userId) return [];
43
+ try {
44
+ const db = requireFirestore();
45
+ const collectionPath = config.useUserSubcollection
46
+ ? `users/${userId}/${config.collectionName}`
47
+ : config.collectionName;
46
48
 
47
- const result = await repository.getTransactions({
48
- userId,
49
- limit,
50
- });
49
+ const q = query(
50
+ collection(db, collectionPath),
51
+ orderBy("timestamp", "desc"),
52
+ limit(limitCount)
53
+ ) as Query;
51
54
 
52
- if (!result.success) {
53
- throw new Error(result.error?.message || "Failed to fetch history");
54
- }
55
+ const unsubscribe = onSnapshot(
56
+ q,
57
+ (snapshot) => {
58
+ const logs: CreditLog[] = [];
59
+ snapshot.forEach((doc) => {
60
+ logs.push({
61
+ id: doc.id,
62
+ ...doc.data(),
63
+ } as CreditLog);
64
+ });
65
+ setTransactions(logs);
66
+ setIsLoading(false);
67
+ },
68
+ (err) => {
69
+ console.error("[useTransactionHistory] Snapshot error:", err);
70
+ setError(err as Error);
71
+ setIsLoading(false);
72
+ }
73
+ );
55
74
 
56
- return result.data ?? [];
57
- },
58
- enabled: hasUser,
59
- ...MEDIUM_CACHE_CONFIG,
60
- });
75
+ return () => unsubscribe();
76
+ } catch (err) {
77
+ const error = err instanceof Error ? err : new Error(String(err));
78
+ console.error("[useTransactionHistory] Setup error:", err);
79
+ setError(error);
80
+ setIsLoading(false);
81
+ }
82
+ }, [userId, config.collectionName, config.useUserSubcollection, limitCount]);
61
83
 
62
- const transactions = data ?? [];
84
+ const refetch = () => {
85
+ if (__DEV__) {
86
+ console.warn("[useTransactionHistory] Refetch called - not needed for real-time sync");
87
+ }
88
+ };
63
89
 
64
90
  return {
65
91
  transactions,
66
92
  isLoading,
67
- error: error as Error | null,
93
+ error,
68
94
  refetch,
69
95
  isEmpty: transactions.length === 0,
70
96
  };
@@ -1,6 +0,0 @@
1
- export const SUBSCRIPTION_QUERY_KEYS = {
2
- packages: ["subscription", "packages"] as const,
3
- initialized: (userId: string) =>
4
- ["subscription", "initialized", userId] as const,
5
- customerInfo: ["subscription", "customerInfo"] as const,
6
- } as const;
@@ -1,22 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import type { QueryClient } from "@umituz/react-native-design-system/tanstack";
3
- import { isAuthenticated } from "../../../../domains/subscription/utils/authGuards";
4
-
5
- export function usePreviousUserCleanup(
6
- userId: string | null | undefined,
7
- queryClient: QueryClient,
8
- queryKey: (userId: string) => readonly unknown[]
9
- ): void {
10
- const prevUserIdRef = useRef(userId);
11
-
12
- useEffect(() => {
13
- const prevUserId = prevUserIdRef.current;
14
- prevUserIdRef.current = userId;
15
-
16
- if (prevUserId !== userId && isAuthenticated(prevUserId)) {
17
- queryClient.removeQueries({
18
- queryKey: queryKey(prevUserId),
19
- });
20
- }
21
- }, [userId, queryClient, queryKey]);
22
- }
@@ -1,38 +0,0 @@
1
- /**
2
- * Query cache configurations for optimal performance
3
- * Uses event-based invalidation via subscriptionEventBus for real-time updates
4
- */
5
-
6
- /**
7
- * Short-lived cache for frequently changing data (credits, subscription status)
8
- * Events automatically invalidate the cache, so we can safely cache for 60s
9
- */
10
- export const SHORT_CACHE_CONFIG = {
11
- gcTime: 1000 * 60, // 1 minute - keep in memory for 1 minute
12
- staleTime: 1000 * 30, // 30 seconds - consider stale after 30s
13
- refetchOnMount: false, // Don't refetch on mount if cache exists
14
- refetchOnWindowFocus: false, // Don't refetch on app focus
15
- refetchOnReconnect: true, // Refetch on reconnect only
16
- };
17
-
18
- /**
19
- * Medium cache for relatively stable data (packages, transaction history)
20
- */
21
- export const MEDIUM_CACHE_CONFIG = {
22
- gcTime: 1000 * 60 * 5, // 5 minutes
23
- staleTime: 1000 * 60 * 2, // 2 minutes
24
- refetchOnMount: false,
25
- refetchOnWindowFocus: false,
26
- refetchOnReconnect: true,
27
- };
28
-
29
- /**
30
- * Long cache for rarely changing data (config, metadata)
31
- */
32
- export const LONG_CACHE_CONFIG = {
33
- gcTime: 1000 * 60 * 30, // 30 minutes
34
- staleTime: 1000 * 60 * 10, // 10 minutes
35
- refetchOnMount: false,
36
- refetchOnWindowFocus: false,
37
- refetchOnReconnect: true,
38
- };