@umituz/react-native-subscription 2.27.139 → 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 +1 -1
- package/src/domains/credits/presentation/useCredits.ts +7 -5
- package/src/domains/credits/utils/creditValidation.ts +27 -0
- package/src/domains/subscription/presentation/usePremium.ts +7 -7
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +11 -5
- package/src/domains/subscription/utils/authGuards.ts +5 -0
- package/src/domains/subscription/utils/syncStatus.ts +21 -0
- package/src/shared/utils/numberUtils.core.ts +3 -35
- package/src/shared/utils/queryKeyFactory.ts +9 -0
- package/src/shared/utils/typeGuards.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
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",
|
|
@@ -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 =
|
|
49
|
+
const queryEnabled = isAuthenticated(userId) && isConfigured;
|
|
48
50
|
|
|
49
51
|
const { data, status, error, refetch } = useQuery({
|
|
50
|
-
queryKey: 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);
|
|
@@ -74,7 +76,7 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
74
76
|
}, [queryClient]);
|
|
75
77
|
|
|
76
78
|
useEffect(() => {
|
|
77
|
-
if (!userId) return;
|
|
79
|
+
if (!isAuthenticated(userId)) return;
|
|
78
80
|
|
|
79
81
|
const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
|
|
80
82
|
if (updatedUserId === userId) {
|
|
@@ -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
|
+
};
|
|
@@ -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 => {
|
|
@@ -24,12 +24,12 @@ export const usePremium = (): UsePremiumResult => {
|
|
|
24
24
|
const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
|
|
25
25
|
|
|
26
26
|
const isPremium = subscriptionActive || (credits?.isPremium ?? false);
|
|
27
|
-
const isSyncing =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
subscriptionActive
|
|
31
|
-
credits
|
|
32
|
-
|
|
27
|
+
const isSyncing = isPremiumSyncPending({
|
|
28
|
+
statusLoading,
|
|
29
|
+
creditsLoading,
|
|
30
|
+
subscriptionActive,
|
|
31
|
+
credits,
|
|
32
|
+
});
|
|
33
33
|
|
|
34
34
|
const handlePurchase = useCallback(
|
|
35
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,12 +16,16 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
14
16
|
const userId = useAuthStore(selectUserId);
|
|
15
17
|
const queryClient = useQueryClient();
|
|
16
18
|
|
|
17
|
-
const queryEnabled =
|
|
19
|
+
const queryEnabled = isAuthenticated(userId) && SubscriptionManager.isInitializedForUser(userId);
|
|
18
20
|
|
|
19
21
|
const { data, status, error, refetch } = useQuery({
|
|
20
|
-
queryKey:
|
|
22
|
+
queryKey: createUserQueryKey(
|
|
23
|
+
subscriptionStatusQueryKeys.all,
|
|
24
|
+
userId,
|
|
25
|
+
subscriptionStatusQueryKeys.user
|
|
26
|
+
),
|
|
21
27
|
queryFn: async () => {
|
|
22
|
-
if (!userId) {
|
|
28
|
+
if (!isAuthenticated(userId)) {
|
|
23
29
|
return { isPremium: false, expirationDate: null };
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -34,10 +40,10 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
34
40
|
});
|
|
35
41
|
|
|
36
42
|
useEffect(() => {
|
|
37
|
-
if (!userId) return;
|
|
43
|
+
if (!isAuthenticated(userId)) return;
|
|
38
44
|
|
|
39
45
|
const unsubscribe = subscriptionEventBus.on(
|
|
40
|
-
SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
|
|
46
|
+
SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED,
|
|
41
47
|
(event: { userId: string; isPremium: boolean }) => {
|
|
42
48
|
if (event.userId === userId) {
|
|
43
49
|
queryClient.invalidateQueries({
|
|
@@ -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,72 +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
|
-
|
|
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
|
-
|
|
71
|
-
return calculatePercentageClamped(current, max);
|
|
39
|
+
return calculateSafePercentage(current, max);
|
|
72
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
|
+
};
|