@umituz/react-native-subscription 2.27.138 → 2.27.140

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.138",
3
+ "version": "2.27.140",
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 { 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 type { UserCredits } from "../core/Credits";
@@ -9,6 +9,8 @@ import {
9
9
  isCreditsRepositoryConfigured,
10
10
  } from "../infrastructure/CreditsRepositoryManager";
11
11
  import { calculateCreditPercentage, canAfford as canAffordCheck } from "../../../shared/utils/numberUtils";
12
+ import { createUserQueryKey } from "../../../shared/utils/queryKeyFactory";
13
+ import { isAuthenticated } from "../../subscription/utils/authGuards";
12
14
 
13
15
  export const creditsQueryKeys = {
14
16
  all: ["credits"] as const,
@@ -42,14 +44,14 @@ function deriveLoadStatus(
42
44
  export const useCredits = (): UseCreditsResult => {
43
45
  const userId = useAuthStore(selectUserId);
44
46
  const isConfigured = isCreditsRepositoryConfigured();
45
-
47
+
46
48
  const config = isConfigured ? getCreditsConfig() : null;
47
- const queryEnabled = !!userId && isConfigured;
49
+ const queryEnabled = isAuthenticated(userId) && isConfigured;
48
50
 
49
51
  const { data, status, error, refetch } = useQuery({
50
- queryKey: creditsQueryKeys.user(userId ?? ""),
52
+ queryKey: createUserQueryKey(creditsQueryKeys.all, userId, creditsQueryKeys.user),
51
53
  queryFn: async () => {
52
- if (!userId || !isConfigured) return null;
54
+ if (!isAuthenticated(userId) || !isConfigured) return null;
53
55
 
54
56
  const repository = getCreditsRepository();
55
57
  const result = await repository.getCredits(userId);
@@ -67,18 +69,23 @@ export const useCredits = (): UseCreditsResult => {
67
69
  });
68
70
 
69
71
  const queryClient = useQueryClient();
72
+ const queryClientRef = useRef(queryClient);
73
+
74
+ useEffect(() => {
75
+ queryClientRef.current = queryClient;
76
+ }, [queryClient]);
70
77
 
71
78
  useEffect(() => {
72
- if (!userId) return;
79
+ if (!isAuthenticated(userId)) return;
73
80
 
74
81
  const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
75
82
  if (updatedUserId === userId) {
76
- queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
83
+ queryClientRef.current.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
77
84
  }
78
85
  });
79
86
 
80
87
  return unsubscribe;
81
- }, [userId, queryClient]);
88
+ }, [userId]);
82
89
 
83
90
  const credits = data ?? null;
84
91
 
@@ -0,0 +1,27 @@
1
+ import { isDefined, isValidNumber, isNonNegative } from "../../../shared/utils/typeGuards";
2
+
3
+ export const isValidBalance = (balance: number | null | undefined): balance is number => {
4
+ return isValidNumber(balance) && isNonNegative(balance);
5
+ };
6
+
7
+ export const isValidCost = (cost: number): boolean => {
8
+ return isValidNumber(cost) && isNonNegative(cost);
9
+ };
10
+
11
+ export const isValidMaxCredits = (max: number): boolean => {
12
+ return isValidNumber(max) && max > 0;
13
+ };
14
+
15
+ export const canAffordAmount = (balance: number | null | undefined, cost: number): boolean => {
16
+ if (!isValidBalance(balance) || !isValidCost(cost)) return false;
17
+ return balance >= cost;
18
+ };
19
+
20
+ export const calculateSafePercentage = (
21
+ current: number | null | undefined,
22
+ max: number
23
+ ): number => {
24
+ if (!isValidNumber(current) || !isValidMaxCredits(max)) return 0;
25
+ const percentage = (current / max) * 100;
26
+ return Math.min(Math.max(percentage, 0), 100);
27
+ };
@@ -14,7 +14,7 @@
14
14
  * @see https://www.revenuecat.com/docs/customers/customer-info
15
15
  */
16
16
 
17
- import { useEffect, useState, useCallback } from "react";
17
+ import { useEffect, useState, useCallback, useRef } from "react";
18
18
  import Purchases, { type CustomerInfo } from "react-native-purchases";
19
19
 
20
20
  export interface UseCustomerInfoResult {
@@ -81,21 +81,23 @@ export function useCustomerInfo(): UseCustomerInfoResult {
81
81
  }
82
82
  }, []);
83
83
 
84
+ const listenerRef = useRef<((info: CustomerInfo) => void) | null>(null);
85
+
84
86
  useEffect(() => {
85
- // Initial fetch
86
87
  fetchCustomerInfo();
87
88
 
88
- // Listen for real-time updates (renewals, purchases, restore)
89
89
  const listener = (info: CustomerInfo) => {
90
90
  setCustomerInfo(info);
91
91
  setError(null);
92
92
  };
93
93
 
94
+ listenerRef.current = listener;
94
95
  Purchases.addCustomerInfoUpdateListener(listener);
95
96
 
96
- // Cleanup listener on unmount
97
97
  return () => {
98
- Purchases.removeCustomerInfoUpdateListener(listener);
98
+ if (listenerRef.current) {
99
+ Purchases.removeCustomerInfoUpdateListener(listenerRef.current);
100
+ }
99
101
  };
100
102
  }, [fetchCustomerInfo]);
101
103
 
@@ -66,6 +66,7 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
66
66
  highlight={
67
67
  daysRemaining !== null &&
68
68
  daysRemaining !== undefined &&
69
+ daysRemaining > 0 &&
69
70
  daysRemaining <= 7
70
71
  }
71
72
  />
@@ -8,7 +8,7 @@ import {
8
8
  useRestorePurchase,
9
9
  } from '../infrastructure/hooks/useSubscriptionQueries';
10
10
  import { usePaywallVisibility } from './usePaywallVisibility';
11
-
11
+ import { isPremiumSyncPending } from '../utils/syncStatus';
12
12
  import { UsePremiumResult } from './usePremium.types';
13
13
 
14
14
  export const usePremium = (): UsePremiumResult => {
@@ -22,9 +22,14 @@ export const usePremium = (): UsePremiumResult => {
22
22
  const restoreMutation = useRestorePurchase();
23
23
 
24
24
  const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
25
-
25
+
26
26
  const isPremium = subscriptionActive || (credits?.isPremium ?? false);
27
- const isSyncing = subscriptionActive && credits !== null && !credits.isPremium;
27
+ const isSyncing = isPremiumSyncPending({
28
+ statusLoading,
29
+ creditsLoading,
30
+ subscriptionActive,
31
+ credits,
32
+ });
28
33
 
29
34
  const handlePurchase = useCallback(
30
35
  async (pkg: PurchasesPackage): Promise<boolean> => {
@@ -4,6 +4,8 @@ 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";
6
6
  import { SubscriptionStatusResult } from "./useSubscriptionStatus.types";
7
+ import { createUserQueryKey } from "../../../shared/utils/queryKeyFactory";
8
+ import { isAuthenticated } from "../utils/authGuards";
7
9
 
8
10
  export const subscriptionStatusQueryKeys = {
9
11
  all: ["subscriptionStatus"] as const,
@@ -14,10 +16,16 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
14
16
  const userId = useAuthStore(selectUserId);
15
17
  const queryClient = useQueryClient();
16
18
 
19
+ const queryEnabled = isAuthenticated(userId) && SubscriptionManager.isInitializedForUser(userId);
20
+
17
21
  const { data, status, error, refetch } = useQuery({
18
- queryKey: subscriptionStatusQueryKeys.user(userId ?? ""),
22
+ queryKey: createUserQueryKey(
23
+ subscriptionStatusQueryKeys.all,
24
+ userId,
25
+ subscriptionStatusQueryKeys.user
26
+ ),
19
27
  queryFn: async () => {
20
- if (!userId) {
28
+ if (!isAuthenticated(userId)) {
21
29
  return { isPremium: false, expirationDate: null };
22
30
  }
23
31
 
@@ -28,14 +36,14 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
28
36
  return { isPremium: false, expirationDate: null };
29
37
  }
30
38
  },
31
- enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
39
+ enabled: queryEnabled,
32
40
  });
33
41
 
34
42
  useEffect(() => {
35
- if (!userId) return;
43
+ if (!isAuthenticated(userId)) return;
36
44
 
37
45
  const unsubscribe = subscriptionEventBus.on(
38
- SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
46
+ SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
39
47
  (event: { userId: string; isPremium: boolean }) => {
40
48
  if (event.userId === userId) {
41
49
  queryClient.invalidateQueries({
@@ -0,0 +1,5 @@
1
+ import { isDefined } from "../../../shared/utils/typeGuards";
2
+
3
+ export const isAuthenticated = (userId: string | null | undefined): userId is string => {
4
+ return isDefined(userId) && userId.length > 0;
5
+ };
@@ -0,0 +1,21 @@
1
+ import { isDefined } from "../../../shared/utils/typeGuards";
2
+ import type { UserCredits } from "../../credits/core/Credits";
3
+
4
+ export interface SyncState {
5
+ statusLoading: boolean;
6
+ creditsLoading: boolean;
7
+ subscriptionActive: boolean;
8
+ credits: UserCredits | null;
9
+ }
10
+
11
+ export const isPremiumSyncPending = (state: SyncState): boolean => {
12
+ const { statusLoading, creditsLoading, subscriptionActive, credits } = state;
13
+
14
+ if (statusLoading || creditsLoading) return false;
15
+
16
+ if (!subscriptionActive) return false;
17
+
18
+ if (!isDefined(credits)) return false;
19
+
20
+ return !credits.isPremium;
21
+ };
@@ -1,73 +1,40 @@
1
- /**
2
- * Number Utilities - Core Operations
3
- * Basic numeric calculation functions
4
- */
1
+ import { canAffordAmount, calculateSafePercentage } from "../../domains/credits/utils/creditValidation";
5
2
 
6
- /**
7
- * Clamp a number between min and max values
8
- */
9
3
  export function clamp(value: number, min: number, max: number): number {
10
4
  return Math.min(Math.max(value, min), max);
11
5
  }
12
6
 
13
- /**
14
- * Round a number to specified decimal places
15
- */
16
7
  export function roundTo(value: number, decimals: number = 2): number {
17
8
  const multiplier = Math.pow(10, decimals);
18
9
  return Math.round(value * multiplier) / multiplier;
19
10
  }
20
11
 
21
- /**
22
- * Calculate percentage
23
- */
24
12
  export function calculatePercentage(value: number, total: number): number {
25
13
  if (total === 0) return 0;
26
14
  return (value / total) * 100;
27
15
  }
28
16
 
29
- /**
30
- * Calculate percentage and clamp between 0-100
31
- */
32
17
  export function calculatePercentageClamped(value: number, total: number): number {
33
18
  return clamp(calculatePercentage(value, total), 0, 100);
34
19
  }
35
20
 
36
- /**
37
- * Check if two numbers are approximately equal (within epsilon)
38
- */
39
21
  export function isApproximatelyEqual(a: number, b: number, epsilon: number = 0.0001): boolean {
40
22
  return Math.abs(a - b) < epsilon;
41
23
  }
42
24
 
43
- /**
44
- * Safe division that returns 0 instead of NaN
45
- */
46
25
  export function safeDivide(numerator: number, denominator: number): number {
47
26
  if (denominator === 0) return 0;
48
27
  return numerator / denominator;
49
28
  }
50
29
 
51
- /**
52
- * Calculate remaining value after subtraction with floor at 0
53
- */
54
30
  export function calculateRemaining(current: number, cost: number): number {
55
31
  return Math.max(0, current - cost);
56
32
  }
57
33
 
58
- /**
59
- * Check if user can afford a cost
60
- */
61
34
  export function canAfford(balance: number | null | undefined, cost: number): boolean {
62
- if (balance === null || balance === undefined) return false;
63
- return balance >= cost;
35
+ return canAffordAmount(balance, cost);
64
36
  }
65
37
 
66
- /**
67
- * Calculate credit percentage for UI display
68
- */
69
38
  export function calculateCreditPercentage(current: number | null | undefined, max: number): number {
70
- if (current === null || current === undefined) return 0;
71
- if (max <= 0) return 100;
72
- return calculatePercentageClamped(current, max);
39
+ return calculateSafePercentage(current, max);
73
40
  }
@@ -0,0 +1,9 @@
1
+ import { isAuthenticated } from "../../domains/subscription/utils/authGuards";
2
+
3
+ export const createUserQueryKey = <T extends readonly unknown[]>(
4
+ baseKey: T,
5
+ userId: string | null | undefined,
6
+ userSpecificKey: (userId: string) => readonly unknown[]
7
+ ): readonly unknown[] => {
8
+ return isAuthenticated(userId) ? userSpecificKey(userId) : baseKey;
9
+ };
@@ -0,0 +1,15 @@
1
+ export const isDefined = <T>(value: T | null | undefined): value is T => {
2
+ return value !== null && value !== undefined;
3
+ };
4
+
5
+ export const isPositive = (value: number): boolean => {
6
+ return value > 0;
7
+ };
8
+
9
+ export const isNonNegative = (value: number): boolean => {
10
+ return value >= 0;
11
+ };
12
+
13
+ export const isValidNumber = (value: number | null | undefined): value is number => {
14
+ return isDefined(value) && !isNaN(value) && isFinite(value);
15
+ };