@umituz/react-native-subscription 2.35.15 → 2.35.16

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.35.15",
3
+ "version": "2.35.16",
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",
@@ -32,7 +32,11 @@ export const getCreditLimitForPlan = (
32
32
  planId: string
33
33
  ): number => {
34
34
  const plan = getPlanById(config, planId);
35
- return plan?.credits ?? 0;
35
+ if (!plan) {
36
+ console.warn(`[planSelectors] Plan not found: ${planId}, returning 0`);
37
+ return 0;
38
+ }
39
+ return plan.credits;
36
40
  };
37
41
 
38
42
  export const determinePlanFromCredits = (
@@ -1,5 +1,5 @@
1
1
  import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
2
- import { useCallback, useMemo, useEffect } from "react";
2
+ import { useCallback, useMemo, useEffect, useRef } from "react";
3
3
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
5
  import {
@@ -44,15 +44,30 @@ export const useCredits = (): UseCreditsResult => {
44
44
  return result.data ?? null;
45
45
  },
46
46
  enabled: queryEnabled,
47
- gcTime: 10 * 60 * 1000,
48
- staleTime: 30 * 1000,
49
- refetchOnMount: true,
50
- refetchOnWindowFocus: false,
51
- refetchOnReconnect: true,
47
+ gcTime: 0,
48
+ staleTime: 0,
49
+ refetchOnMount: "always",
50
+ refetchOnWindowFocus: "always",
51
+ refetchOnReconnect: "always",
52
52
  });
53
53
 
54
54
  const queryClient = useQueryClient();
55
55
 
56
+ // Track previous userId to clear stale cache on logout/user switch
57
+ const prevUserIdRef = useRef(userId);
58
+
59
+ useEffect(() => {
60
+ const prevUserId = prevUserIdRef.current;
61
+ prevUserIdRef.current = userId;
62
+
63
+ // Clear previous user's cache when userId changes (logout or user switch)
64
+ if (prevUserId !== userId && isAuthenticated(prevUserId)) {
65
+ queryClient.removeQueries({
66
+ queryKey: creditsQueryKeys.user(prevUserId),
67
+ });
68
+ }
69
+ }, [userId, queryClient]);
70
+
56
71
  useEffect(() => {
57
72
  if (!isAuthenticated(userId)) return undefined;
58
73
 
@@ -43,7 +43,7 @@ export class SubscriptionSyncProcessor {
43
43
 
44
44
  async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
45
45
  const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
46
- revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
46
+ revenueCatData.expirationDate = newExpirationDate ?? revenueCatData.expirationDate;
47
47
  const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
48
48
 
49
49
  const creditsUserId = await this.getCreditsUserId(userId);
@@ -4,8 +4,8 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
4
  export function getApiKey(config: SubscriptionInitConfig): string {
5
5
  const { apiKey, apiKeyIos, apiKeyAndroid } = config;
6
6
  const key = Platform.OS === 'ios'
7
- ? (apiKeyIos || apiKey)
8
- : (apiKeyAndroid || apiKey);
7
+ ? (apiKeyIos ?? apiKey)
8
+ : (apiKeyAndroid ?? apiKey);
9
9
 
10
10
  if (!key) {
11
11
  throw new Error('API key required');
@@ -3,6 +3,8 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
3
3
  import { configureAuthProvider } from "../../presentation/useAuthAwarePurchase";
4
4
  import { SubscriptionSyncService } from "../SubscriptionSyncService";
5
5
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
6
+ import type { CustomerInfo } from "react-native-purchases";
7
+ import type { PackageType } from "../../../revenuecat/core/types/RevenueCatTypes";
6
8
 
7
9
  export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
8
10
  const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated, getAnonymousUserId } = config;
@@ -23,9 +25,27 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
23
25
  apiKey,
24
26
  entitlementIdentifier: entitlementId,
25
27
  consumableProductIdentifiers: [creditPackages.identifierPattern],
26
- onPurchaseCompleted: (u: string, p: string, c: any, s: any, pkgType: any) => syncService.handlePurchase(u, p, c, s, pkgType),
27
- onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
28
- onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
28
+ onPurchaseCompleted: (
29
+ u: string,
30
+ p: string,
31
+ c: CustomerInfo,
32
+ s?: string,
33
+ pkgType?: PackageType | null
34
+ ) => syncService.handlePurchase(u, p, c, s as any, pkgType),
35
+ onRenewalDetected: (
36
+ u: string,
37
+ p: string,
38
+ expires: string,
39
+ c: CustomerInfo
40
+ ) => syncService.handleRenewal(u, p, expires, c),
41
+ onPremiumStatusChanged: (
42
+ u: string,
43
+ isP: boolean,
44
+ pId?: string,
45
+ exp?: string,
46
+ willR?: boolean,
47
+ pt?: string
48
+ ) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt as any),
29
49
  onCreditsUpdated,
30
50
  },
31
51
  apiKey,
@@ -45,7 +45,7 @@ export function useCustomerInfo(): UseCustomerInfoResult {
45
45
  listenerRef.current = null;
46
46
  }
47
47
  };
48
- }, [fetchCustomerInfo]);
48
+ }, []); // fetchCustomerInfo is stable, setup listener once
49
49
 
50
50
  return {
51
51
  customerInfo,
@@ -29,17 +29,27 @@ export const usePaywallFlow = (options: UsePaywallFlowOptions = {}): UsePaywallF
29
29
 
30
30
  // Load persisted state
31
31
  useEffect(() => {
32
+ let isMounted = true;
33
+
32
34
  const loadPersistedState = async () => {
33
35
  try {
34
36
  const value = await getString(PAYWALL_SHOWN_KEY, '');
35
- setPaywallShown(value === 'true');
37
+ if (isMounted) {
38
+ setPaywallShown(value === 'true');
39
+ }
36
40
  } catch (error) {
37
41
  console.error('[usePaywallFlow] Failed to load paywall state', error);
38
- setPaywallShown(false); // Safe default
42
+ if (isMounted) {
43
+ setPaywallShown(false); // Safe default
44
+ }
39
45
  }
40
46
  };
41
47
 
42
48
  loadPersistedState();
49
+
50
+ return () => {
51
+ isMounted = false;
52
+ };
43
53
  }, [getString]);
44
54
 
45
55
  const closePostOnboardingPaywall = useCallback(async (_isPremium: boolean) => {
@@ -3,7 +3,7 @@
3
3
  * React hook for RevenueCat subscription management
4
4
  */
5
5
 
6
- import { useState, useCallback } from "react";
6
+ import { useState, useCallback, useEffect, useRef } from "react";
7
7
  import type { PurchasesOffering, PurchasesPackage } from "react-native-purchases";
8
8
  import { getRevenueCatService } from '../../infrastructure/services/RevenueCatService';
9
9
  import type { PurchaseResult, RestoreResult } from '../../../../shared/application/ports/IRevenueCatService';
@@ -35,8 +35,18 @@ export interface UseRevenueCatResult {
35
35
  export function useRevenueCat(): UseRevenueCatResult {
36
36
  const [offering, setOffering] = useState<PurchasesOffering | null>(null);
37
37
  const [loading, setLoading] = useState(false);
38
+ const isMountedRef = useRef(true);
39
+
40
+ useEffect(() => {
41
+ isMountedRef.current = true;
42
+ return () => {
43
+ isMountedRef.current = false;
44
+ };
45
+ }, []);
38
46
 
39
47
  const initialize = useCallback(async (userId: string, apiKey?: string) => {
48
+ if (!isMountedRef.current) return;
49
+
40
50
  setLoading(true);
41
51
  try {
42
52
  const service = getRevenueCatService();
@@ -44,17 +54,21 @@ export function useRevenueCat(): UseRevenueCatResult {
44
54
  return;
45
55
  }
46
56
  const result = await service.initialize(userId, apiKey);
47
- if (result.success) {
57
+ if (result.success && isMountedRef.current) {
48
58
  setOffering(result.offering);
49
59
  }
50
60
  } catch {
51
61
  // Error handling is done by service
52
62
  } finally {
53
- setLoading(false);
63
+ if (isMountedRef.current) {
64
+ setLoading(false);
65
+ }
54
66
  }
55
67
  }, []);
56
68
 
57
69
  const loadOfferings = useCallback(async () => {
70
+ if (!isMountedRef.current) return;
71
+
58
72
  setLoading(true);
59
73
  try {
60
74
  const service = getRevenueCatService();
@@ -62,11 +76,15 @@ export function useRevenueCat(): UseRevenueCatResult {
62
76
  return;
63
77
  }
64
78
  const fetchedOffering = await service.fetchOfferings();
65
- setOffering(fetchedOffering);
79
+ if (isMountedRef.current) {
80
+ setOffering(fetchedOffering);
81
+ }
66
82
  } catch {
67
83
  // Error handling is done by service
68
84
  } finally {
69
- setLoading(false);
85
+ if (isMountedRef.current) {
86
+ setLoading(false);
87
+ }
70
88
  }
71
89
  }, []);
72
90
 
@@ -108,7 +108,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
108
108
  }
109
109
 
110
110
  prevCreditBalanceRef.current = creditBalance;
111
- hasSubscriptionRef.current = hasSubscription;
111
+ // hasSubscriptionRef is already synced by useSyncedRefs, no need to update manually
112
112
  // eslint-disable-next-line react-hooks/exhaustive-deps
113
113
  }, [creditBalance, hasSubscription]);
114
114
 
@@ -12,7 +12,7 @@ export class TrialEligibilityService {
12
12
 
13
13
  const { hasUsedTrial, trialInProgress, userIds = [] } = record;
14
14
 
15
- if (userId && userIds.includes(userId)) {
15
+ if (userId && userId.length > 0 && userIds.includes(userId)) {
16
16
  return { eligible: false, reason: "user_already_used", deviceId };
17
17
  }
18
18
 
@@ -26,9 +26,17 @@ const repository = new DeviceTrialRepository();
26
26
 
27
27
  export const getDeviceId = () => PersistentDeviceIdService.getDeviceId();
28
28
 
29
+ /**
30
+ * Ensures a valid device ID is available
31
+ * Uses provided deviceId if non-empty, otherwise fetches from PersistentDeviceIdService
32
+ */
33
+ async function ensureDeviceId(deviceId?: string): Promise<string> {
34
+ return (deviceId && deviceId.length > 0) ? deviceId : await getDeviceId();
35
+ }
36
+
29
37
  export async function checkTrialEligibility(userId?: string, deviceId?: string): Promise<TrialEligibilityResult> {
30
38
  try {
31
- const id = deviceId || await getDeviceId();
39
+ const id = await ensureDeviceId(deviceId);
32
40
  const record = await repository.getRecord(id);
33
41
  return TrialEligibilityService.check(userId, id, record);
34
42
  } catch {
@@ -38,7 +46,7 @@ export async function checkTrialEligibility(userId?: string, deviceId?: string):
38
46
 
39
47
  export async function recordTrialStart(userId: string, deviceId?: string): Promise<boolean> {
40
48
  try {
41
- const id = deviceId || await getDeviceId();
49
+ const id = await ensureDeviceId(deviceId);
42
50
  const record: TrialRecordWrite = {
43
51
  deviceId: id,
44
52
  trialInProgress: true,
@@ -54,7 +62,7 @@ export async function recordTrialStart(userId: string, deviceId?: string): Promi
54
62
 
55
63
  export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
56
64
  try {
57
- const id = deviceId || await getDeviceId();
65
+ const id = await ensureDeviceId(deviceId);
58
66
  const record: TrialRecordWrite = {
59
67
  hasUsedTrial: true,
60
68
  trialInProgress: false,
@@ -68,7 +76,7 @@ export async function recordTrialEnd(deviceId?: string): Promise<boolean> {
68
76
 
69
77
  export async function recordTrialConversion(deviceId?: string): Promise<boolean> {
70
78
  try {
71
- const id = deviceId || await getDeviceId();
79
+ const id = await ensureDeviceId(deviceId);
72
80
  const record: TrialRecordWrite = {
73
81
  hasUsedTrial: true,
74
82
  trialInProgress: false,
@@ -34,7 +34,7 @@ export async function addTransaction(
34
34
  change,
35
35
  reason,
36
36
  ...metadata,
37
- createdAt: Date.now(),
37
+ createdAt: Date.now(), // Approximate - actual server timestamp written to Firestore
38
38
  },
39
39
  };
40
40
  } catch (error) {
@@ -34,7 +34,7 @@ export function useProductMetadata({
34
34
  }: UseProductMetadataParams): UseProductMetadataResult {
35
35
  const service = useMemo(
36
36
  () => new ProductMetadataService(config),
37
- [config]
37
+ [config.collectionName, config.cacheTTL]
38
38
  );
39
39
 
40
40
  const queryKey = type
@@ -33,7 +33,7 @@ export function useTransactionHistory({
33
33
 
34
34
  const repository = useMemo(
35
35
  () => new TransactionRepository(config),
36
- [config]
36
+ [config.collectionName, config.useUserSubcollection]
37
37
  );
38
38
 
39
39
  const { data, isLoading, error, refetch } = useQuery({