@umituz/react-native-subscription 2.31.9 → 2.31.11

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.31.9",
3
+ "version": "2.31.11",
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,5 +1,5 @@
1
1
  import { getDoc, setDoc } from "firebase/firestore";
2
- import { BaseRepository, type Firestore, type DocumentReference } from "@umituz/react-native-firebase";
2
+ import { BaseRepository, serverTimestamp, type Firestore, type DocumentReference } from "@umituz/react-native-firebase";
3
3
  import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
4
4
  import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
5
5
  import { initializeCreditsTransaction } from "../application/CreditsInitializer";
@@ -146,7 +146,7 @@ export class CreditsRepository extends BaseRepository {
146
146
  isPremium: false,
147
147
  status: SUBSCRIPTION_STATUS.EXPIRED,
148
148
  willRenew: false,
149
- expirationDate: new Date().toISOString()
149
+ expirationDate: serverTimestamp(),
150
150
  }, { merge: true });
151
151
  }
152
152
  }
@@ -44,9 +44,11 @@ export const useCredits = (): UseCreditsResult => {
44
44
  return result.data || null;
45
45
  },
46
46
  enabled: queryEnabled,
47
+ gcTime: 0,
48
+ staleTime: 0,
47
49
  refetchOnMount: "always",
48
- refetchOnWindowFocus: true,
49
- refetchOnReconnect: true,
50
+ refetchOnWindowFocus: "always",
51
+ refetchOnReconnect: "always",
50
52
  });
51
53
 
52
54
  const queryClient = useQueryClient();
@@ -40,6 +40,10 @@ export const useSubscriptionPackages = () => {
40
40
  return SubscriptionManager.getPackages();
41
41
  },
42
42
  enabled: isConfigured,
43
-
43
+ gcTime: 0,
44
+ staleTime: 0,
45
+ refetchOnMount: "always",
46
+ refetchOnWindowFocus: "always",
47
+ refetchOnReconnect: "always",
44
48
  });
45
49
  };
@@ -9,7 +9,8 @@ export async function fetchOfferings(deps: OfferingsFetcherDeps): Promise<Purcha
9
9
  try {
10
10
  const offerings = await Purchases.getOfferings();
11
11
  return offerings.current;
12
- } catch {
12
+ } catch (error) {
13
+ console.error('[OfferingsFetcher] Failed to fetch offerings', { error });
13
14
  return null;
14
15
  }
15
16
  }
@@ -125,6 +125,13 @@ export async function handlePurchase(
125
125
  ? `${errorMessage} (Code: ${errorCode})`
126
126
  : errorMessage;
127
127
 
128
+ console.error('[PurchaseHandler] Purchase failed', {
129
+ productId: pkg.product.identifier,
130
+ userId,
131
+ errorCode,
132
+ error,
133
+ });
134
+
128
135
  throw new RevenueCatPurchaseError(
129
136
  enhancedMessage,
130
137
  pkg.product.identifier,
@@ -80,8 +80,8 @@ export class RevenueCatService implements IRevenueCatService {
80
80
  try {
81
81
  await Purchases.logOut();
82
82
  this.stateManager.setInitialized(false);
83
- } catch {
84
- // Ignore logout errors during reset
83
+ } catch (error) {
84
+ console.error('[RevenueCatService] Logout failed during reset', { error });
85
85
  }
86
86
  }
87
87
  }
@@ -40,35 +40,41 @@ export class InitializationCache {
40
40
  }
41
41
 
42
42
  // If we reach here, initialization is in progress for a different user
43
- // Wait for current initialization to complete
44
- return { shouldInit: false, existingPromise: this.initPromise };
43
+ // Don't return another user's promise - caller should retry
44
+ return { shouldInit: false, existingPromise: null };
45
45
  }
46
46
 
47
47
  setPromise(promise: Promise<boolean>, userId: string): void {
48
48
  this.initPromise = promise;
49
49
  this.promiseUserId = userId;
50
50
 
51
+ // Capture userId to prevent stale reference after catch clears promiseUserId
52
+ const targetUserId = userId;
53
+
51
54
  // Chain to mark completion and set currentUserId only on success
52
55
  promise
53
56
  .then((result) => {
54
- if (result && this.promiseUserId === userId) {
55
- this.currentUserId = userId;
57
+ if (result && this.promiseUserId === targetUserId) {
58
+ this.currentUserId = targetUserId;
56
59
  }
57
60
  this.promiseCompleted = true;
58
61
  return result;
59
62
  })
60
- .catch(() => {
63
+ .catch((error) => {
61
64
  // On failure, clear the promise so retry is possible
62
- if (this.promiseUserId === userId) {
65
+ if (this.promiseUserId === targetUserId) {
63
66
  this.initPromise = null;
64
67
  this.promiseUserId = null;
65
- this.currentUserId = null; // Clear user on failure
68
+ this.currentUserId = null;
66
69
  }
67
70
  this.promiseCompleted = true;
71
+ console.error('[InitializationCache] Initialization failed', { userId: targetUserId, error });
72
+ // Re-throw so callers awaiting the promise see the error
73
+ throw error;
68
74
  })
69
75
  .finally(() => {
70
76
  // Always release the mutex
71
- if (this.promiseUserId === userId) {
77
+ if (this.promiseUserId === targetUserId) {
72
78
  this.initializationInProgress = false;
73
79
  }
74
80
  });
@@ -1,5 +1,5 @@
1
1
  import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
2
- import { useEffect } from "react";
2
+ import { useEffect, useRef } from "react";
3
3
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
4
  import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
5
5
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
@@ -33,13 +33,24 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
33
33
  }
34
34
  },
35
35
  enabled: queryEnabled,
36
+ gcTime: 0,
37
+ staleTime: 0,
38
+ refetchOnMount: "always",
39
+ refetchOnWindowFocus: "always",
40
+ refetchOnReconnect: "always",
36
41
  });
37
42
 
38
- // Clear cache on logout to prevent stale data
43
+ // Track previous userId to clear stale cache on logout/user switch
44
+ const prevUserIdRef = useRef(userId);
45
+
39
46
  useEffect(() => {
40
- if (!isAuthenticated(userId)) {
47
+ const prevUserId = prevUserIdRef.current;
48
+ prevUserIdRef.current = userId;
49
+
50
+ // Clear previous user's cache when userId changes (logout or user switch)
51
+ if (prevUserId !== userId && isAuthenticated(prevUserId)) {
41
52
  queryClient.removeQueries({
42
- queryKey: subscriptionStatusQueryKeys.user(userId)
53
+ queryKey: subscriptionStatusQueryKeys.user(prevUserId),
43
54
  });
44
55
  }
45
56
  }, [userId, queryClient]);
@@ -1,5 +1,5 @@
1
- import { doc, getDoc, setDoc } from "firebase/firestore";
2
- import { getFirestore, serverTimestamp, type Firestore } from "@umituz/react-native-firebase";
1
+ import { doc } from "firebase/firestore";
2
+ import { getFirestore, runTransaction, serverTimestamp, type Firestore, type Transaction } from "@umituz/react-native-firebase";
3
3
  import type { DeviceTrialRecord } from "../core/TrialTypes";
4
4
 
5
5
  const DEVICE_TRIALS_COLLECTION = "device_trials";
@@ -11,20 +11,34 @@ export class DeviceTrialRepository {
11
11
 
12
12
  async getRecord(deviceId: string): Promise<DeviceTrialRecord | null> {
13
13
  if (!this.db) return null;
14
- const snap = await getDoc(doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId));
14
+ const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
15
+ const snap = await runTransaction(async (tx: Transaction) => {
16
+ return tx.get(ref);
17
+ });
15
18
  return snap.exists() ? snap.data() as DeviceTrialRecord : null;
16
19
  }
17
20
 
18
21
  async saveRecord(deviceId: string, data: Partial<DeviceTrialRecord>): Promise<boolean> {
19
22
  if (!this.db) return false;
20
23
  const ref = doc(this.db, DEVICE_TRIALS_COLLECTION, deviceId);
21
- await setDoc(ref, { ...data, updatedAt: serverTimestamp() }, { merge: true });
22
-
23
- // Ensure createdAt exists
24
- const snap = await getDoc(ref);
25
- if (!snap.data()?.createdAt) {
26
- await setDoc(ref, { createdAt: serverTimestamp() }, { merge: true });
27
- }
24
+
25
+ // Atomic check-then-act: ensure createdAt is set only once
26
+ await runTransaction(async (tx: Transaction) => {
27
+ const snap = await tx.get(ref);
28
+ const existingData = snap.data();
29
+
30
+ const updateData: Record<string, unknown> = {
31
+ ...data,
32
+ updatedAt: serverTimestamp(),
33
+ };
34
+
35
+ if (!existingData?.createdAt) {
36
+ updateData.createdAt = serverTimestamp();
37
+ }
38
+
39
+ tx.set(ref, updateData, { merge: true });
40
+ });
41
+
28
42
  return true;
29
43
  }
30
44
  }
@@ -50,6 +50,11 @@ export function useProductMetadata({
50
50
  return service.getAll();
51
51
  },
52
52
  enabled,
53
+ gcTime: 0,
54
+ staleTime: 0,
55
+ refetchOnMount: "always",
56
+ refetchOnWindowFocus: "always",
57
+ refetchOnReconnect: "always",
53
58
  });
54
59
 
55
60
  const products = data ?? [];
@@ -53,6 +53,11 @@ export function useTransactionHistory({
53
53
  return result.data ?? [];
54
54
  },
55
55
  enabled: !!userId,
56
+ gcTime: 0,
57
+ staleTime: 0,
58
+ refetchOnMount: "always",
59
+ refetchOnWindowFocus: "always",
60
+ refetchOnReconnect: "always",
56
61
  });
57
62
 
58
63
  const transactions = data ?? [];
@@ -42,8 +42,8 @@ export class SubscriptionEventBus {
42
42
  this.listeners[event].forEach(callback => {
43
43
  try {
44
44
  callback(data);
45
- } catch {
46
- // Prevent one faulty listener from breaking other listeners
45
+ } catch (error) {
46
+ console.error('[SubscriptionEventBus] Listener error for event:', event, { error });
47
47
  }
48
48
  });
49
49
  }