@umituz/react-native-subscription 2.26.19 → 2.26.20
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 +2 -1
- package/src/domains/paywall/components/PaywallContainer.tsx +0 -1
- package/src/domains/paywall/hooks/usePaywallActions.ts +11 -11
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +12 -17
- package/src/domains/wallet/presentation/hooks/useWallet.ts +8 -24
- package/src/presentation/hooks/useAuthAwarePurchase.ts +6 -29
- package/src/presentation/hooks/useCredits.ts +27 -73
- package/src/presentation/hooks/usePremium.ts +23 -59
- package/src/presentation/hooks/useSavedPurchaseAutoExecution.ts +13 -64
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +3 -20
- package/src/presentation/hooks/useSubscriptionStatus.ts +11 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.26.
|
|
3
|
+
"version": "2.26.20",
|
|
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@tanstack/react-query": ">=5.0.0",
|
|
36
|
+
"@umituz/react-native-auth": ">=3.0.0",
|
|
36
37
|
"expo-constants": ">=17.0.0",
|
|
37
38
|
"expo-image": ">=3.0.0",
|
|
38
39
|
"firebase": ">=10.0.0",
|
|
@@ -44,7 +44,6 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
44
44
|
const { data: allPackages = [], isLoading } = useSubscriptionPackages(userId ?? undefined);
|
|
45
45
|
const { eligibilityMap, checkEligibility } = useRevenueCatTrialEligibility();
|
|
46
46
|
const { handlePurchase, handleRestore } = usePaywallActions({
|
|
47
|
-
userId: userId ?? undefined,
|
|
48
47
|
source: purchaseSource,
|
|
49
48
|
onPurchaseSuccess,
|
|
50
49
|
onPurchaseError,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
|
+
import {
|
|
4
|
+
useAuthStore,
|
|
5
|
+
selectUserId,
|
|
6
|
+
} from "@umituz/react-native-auth";
|
|
3
7
|
import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
|
|
4
8
|
import { useAuthAwarePurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
|
|
5
9
|
import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
@@ -7,7 +11,6 @@ import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
|
7
11
|
declare const __DEV__: boolean;
|
|
8
12
|
|
|
9
13
|
interface UsePaywallActionsProps {
|
|
10
|
-
userId?: string;
|
|
11
14
|
source?: PurchaseSource;
|
|
12
15
|
onPurchaseSuccess?: () => void;
|
|
13
16
|
onPurchaseError?: (error: string) => void;
|
|
@@ -16,38 +19,35 @@ interface UsePaywallActionsProps {
|
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export const usePaywallActions = ({
|
|
19
|
-
userId,
|
|
20
22
|
source,
|
|
21
23
|
onPurchaseSuccess,
|
|
22
24
|
onPurchaseError,
|
|
23
25
|
onAuthRequired: _onAuthRequired,
|
|
24
26
|
onClose,
|
|
25
27
|
}: UsePaywallActionsProps) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
+
const userId = useAuthStore(selectUserId);
|
|
29
|
+
|
|
30
|
+
const { handlePurchase: authAwarePurchase } = useAuthAwarePurchase({ source });
|
|
31
|
+
const { mutateAsync: restorePurchases } = useRestorePurchase(userId ?? undefined);
|
|
28
32
|
|
|
29
33
|
const handlePurchase = useCallback(async (pkg: PurchasesPackage) => {
|
|
30
34
|
try {
|
|
31
|
-
if (__DEV__
|
|
35
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
36
|
+
console.log("[PaywallActions] Purchase started:", pkg.product.identifier);
|
|
37
|
+
}
|
|
32
38
|
const res = await authAwarePurchase(pkg, source);
|
|
33
|
-
if (__DEV__) console.log("[PaywallActions] Purchase result:", { res, productId: pkg.product.identifier });
|
|
34
39
|
if (res) {
|
|
35
|
-
if (__DEV__) console.log("[PaywallActions] Purchase successful, closing paywall");
|
|
36
40
|
onPurchaseSuccess?.();
|
|
37
41
|
onClose();
|
|
38
|
-
} else {
|
|
39
|
-
if (__DEV__) console.log("[PaywallActions] Purchase returned false, paywall stays open");
|
|
40
42
|
}
|
|
41
43
|
} catch (err: unknown) {
|
|
42
44
|
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
-
if (__DEV__) console.log("[PaywallActions] Purchase error:", message);
|
|
44
45
|
onPurchaseError?.(message);
|
|
45
46
|
}
|
|
46
47
|
}, [authAwarePurchase, source, onClose, onPurchaseSuccess, onPurchaseError]);
|
|
47
48
|
|
|
48
49
|
const handleRestore = useCallback(async () => {
|
|
49
50
|
try {
|
|
50
|
-
if (__DEV__) console.log("[PaywallActions] Restore started");
|
|
51
51
|
const res = await restorePurchases();
|
|
52
52
|
if (res.success) {
|
|
53
53
|
onPurchaseSuccess?.();
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* useTransactionHistory Hook
|
|
3
3
|
*
|
|
4
4
|
* TanStack Query hook for fetching credit transaction history.
|
|
5
|
-
*
|
|
5
|
+
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
9
|
import { useMemo } from "react";
|
|
10
|
+
import {
|
|
11
|
+
useAuthStore,
|
|
12
|
+
selectUserId,
|
|
13
|
+
} from "@umituz/react-native-auth";
|
|
10
14
|
import type {
|
|
11
15
|
CreditLog,
|
|
12
16
|
TransactionRepositoryConfig,
|
|
@@ -15,21 +19,14 @@ import { TransactionRepository } from "../../infrastructure/repositories/Transac
|
|
|
15
19
|
|
|
16
20
|
declare const __DEV__: boolean;
|
|
17
21
|
|
|
18
|
-
const CACHE_CONFIG = {
|
|
19
|
-
staleTime: 60 * 1000, // 1 minute
|
|
20
|
-
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
21
|
-
};
|
|
22
|
-
|
|
23
22
|
export const transactionQueryKeys = {
|
|
24
23
|
all: ["transactions"] as const,
|
|
25
24
|
user: (userId: string) => ["transactions", userId] as const,
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
export interface UseTransactionHistoryParams {
|
|
29
|
-
userId: string | undefined;
|
|
30
28
|
config: TransactionRepositoryConfig;
|
|
31
29
|
limit?: number;
|
|
32
|
-
enabled?: boolean;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
export interface UseTransactionHistoryResult {
|
|
@@ -41,12 +38,11 @@ export interface UseTransactionHistoryResult {
|
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
export function useTransactionHistory({
|
|
44
|
-
userId,
|
|
45
41
|
config,
|
|
46
42
|
limit = 50,
|
|
47
|
-
enabled = true,
|
|
48
43
|
}: UseTransactionHistoryParams): UseTransactionHistoryResult {
|
|
49
|
-
|
|
44
|
+
const userId = useAuthStore(selectUserId);
|
|
45
|
+
|
|
50
46
|
const repository = useMemo(
|
|
51
47
|
() => new TransactionRepository(config),
|
|
52
48
|
[config]
|
|
@@ -68,17 +64,16 @@ export function useTransactionHistory({
|
|
|
68
64
|
|
|
69
65
|
return result.data ?? [];
|
|
70
66
|
},
|
|
71
|
-
enabled:
|
|
72
|
-
staleTime:
|
|
73
|
-
gcTime:
|
|
67
|
+
enabled: !!userId,
|
|
68
|
+
staleTime: 0,
|
|
69
|
+
gcTime: 0,
|
|
74
70
|
});
|
|
75
71
|
|
|
76
72
|
const transactions = data ?? [];
|
|
77
73
|
|
|
78
|
-
if (__DEV__) {
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
79
75
|
console.log("[useTransactionHistory] State", {
|
|
80
|
-
userId,
|
|
81
|
-
enabled,
|
|
76
|
+
userId: userId?.slice(0, 8),
|
|
82
77
|
isLoading,
|
|
83
78
|
count: transactions.length,
|
|
84
79
|
});
|
|
@@ -6,21 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useCallback, useMemo } from "react";
|
|
9
|
+
import { useCredits } from "../../../../presentation/hooks/useCredits";
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "../../../../presentation/hooks/useCredits";
|
|
13
|
-
import {
|
|
14
|
-
useTransactionHistory,
|
|
15
|
-
type UseTransactionHistoryParams,
|
|
11
|
+
useTransactionHistory,
|
|
12
|
+
type UseTransactionHistoryParams,
|
|
16
13
|
} from "./useTransactionHistory";
|
|
17
14
|
import type { CreditLog } from "../../domain/types/transaction.types";
|
|
18
15
|
|
|
19
16
|
export interface UseWalletParams {
|
|
20
|
-
userId: string | undefined;
|
|
21
17
|
transactionConfig: UseTransactionHistoryParams["config"];
|
|
22
18
|
transactionLimit?: number;
|
|
23
|
-
enabled?: boolean;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
export interface UseWalletResult {
|
|
@@ -35,35 +30,24 @@ export interface UseWalletResult {
|
|
|
35
30
|
}
|
|
36
31
|
|
|
37
32
|
export function useWallet({
|
|
38
|
-
userId,
|
|
39
33
|
transactionConfig,
|
|
40
34
|
transactionLimit = 50,
|
|
41
|
-
enabled = true,
|
|
42
35
|
}: UseWalletParams): UseWalletResult {
|
|
43
|
-
const creditsParams: UseCreditsParams = {
|
|
44
|
-
userId,
|
|
45
|
-
enabled,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const transactionParams: UseTransactionHistoryParams = {
|
|
49
|
-
userId,
|
|
50
|
-
config: transactionConfig,
|
|
51
|
-
limit: transactionLimit,
|
|
52
|
-
enabled,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
36
|
const {
|
|
56
37
|
credits,
|
|
57
38
|
isLoading: balanceLoading,
|
|
58
39
|
refetch: refetchBalance,
|
|
59
40
|
hasCredits,
|
|
60
|
-
} = useCredits(
|
|
41
|
+
} = useCredits();
|
|
61
42
|
|
|
62
43
|
const {
|
|
63
44
|
transactions,
|
|
64
45
|
isLoading: transactionsLoading,
|
|
65
46
|
refetch: refetchTransactions,
|
|
66
|
-
} = useTransactionHistory(
|
|
47
|
+
} = useTransactionHistory({
|
|
48
|
+
config: transactionConfig,
|
|
49
|
+
limit: transactionLimit,
|
|
50
|
+
});
|
|
67
51
|
|
|
68
52
|
const balance = useMemo(() => {
|
|
69
53
|
return credits?.credits ?? 0;
|
|
@@ -37,7 +37,6 @@ export const clearSavedPurchase = (): void => {
|
|
|
37
37
|
|
|
38
38
|
export interface UseAuthAwarePurchaseParams {
|
|
39
39
|
source?: PurchaseSource;
|
|
40
|
-
userId?: string;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export interface UseAuthAwarePurchaseResult {
|
|
@@ -49,63 +48,44 @@ export interface UseAuthAwarePurchaseResult {
|
|
|
49
48
|
export const useAuthAwarePurchase = (
|
|
50
49
|
params?: UseAuthAwarePurchaseParams
|
|
51
50
|
): UseAuthAwarePurchaseResult => {
|
|
52
|
-
const { purchasePackage, restorePurchase } = usePremium(
|
|
51
|
+
const { purchasePackage, restorePurchase } = usePremium();
|
|
53
52
|
|
|
54
53
|
const handlePurchase = useCallback(
|
|
55
54
|
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
56
|
-
if (__DEV__) {
|
|
57
|
-
console.log("[useAuthAwarePurchase] handlePurchase
|
|
55
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
+
console.log("[useAuthAwarePurchase] handlePurchase:", {
|
|
58
57
|
productId: pkg.product.identifier,
|
|
59
58
|
hasAuthProvider: !!globalAuthProvider,
|
|
60
59
|
});
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
if (!globalAuthProvider) {
|
|
64
|
-
if (__DEV__) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
65
64
|
console.error("[useAuthAwarePurchase] Auth provider not configured");
|
|
66
65
|
}
|
|
67
66
|
return false;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
const isAuth = globalAuthProvider.isAuthenticated();
|
|
71
|
-
if (__DEV__) {
|
|
72
|
-
console.log("[useAuthAwarePurchase] Auth check:", { isAuthenticated: isAuth });
|
|
73
|
-
}
|
|
74
70
|
|
|
75
71
|
if (!isAuth) {
|
|
76
|
-
if (__DEV__) {
|
|
77
|
-
console.log("[useAuthAwarePurchase] Not authenticated, saving and showing auth");
|
|
78
|
-
}
|
|
79
72
|
savedPackage = pkg;
|
|
80
73
|
savedSource = source || params?.source || "settings";
|
|
81
74
|
globalAuthProvider.showAuthModal();
|
|
82
75
|
return false;
|
|
83
76
|
}
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
console.log("[useAuthAwarePurchase] Calling purchasePackage");
|
|
87
|
-
}
|
|
88
|
-
const result = await purchasePackage(pkg);
|
|
89
|
-
if (__DEV__) {
|
|
90
|
-
console.log("[useAuthAwarePurchase] purchasePackage returned:", result);
|
|
91
|
-
}
|
|
92
|
-
return result;
|
|
78
|
+
return purchasePackage(pkg);
|
|
93
79
|
},
|
|
94
80
|
[purchasePackage, params?.source]
|
|
95
81
|
);
|
|
96
82
|
|
|
97
83
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
98
84
|
if (!globalAuthProvider) {
|
|
99
|
-
if (__DEV__) {
|
|
100
|
-
console.error("[useAuthAwarePurchase] Auth provider not configured");
|
|
101
|
-
}
|
|
102
85
|
return false;
|
|
103
86
|
}
|
|
104
87
|
|
|
105
88
|
if (!globalAuthProvider.isAuthenticated()) {
|
|
106
|
-
if (__DEV__) {
|
|
107
|
-
console.log("[useAuthAwarePurchase] Not authenticated for restore");
|
|
108
|
-
}
|
|
109
89
|
globalAuthProvider.showAuthModal();
|
|
110
90
|
return false;
|
|
111
91
|
}
|
|
@@ -116,13 +96,10 @@ export const useAuthAwarePurchase = (
|
|
|
116
96
|
const executeSavedPurchase = useCallback(async (): Promise<boolean> => {
|
|
117
97
|
const saved = getSavedPurchase();
|
|
118
98
|
if (!saved) {
|
|
119
|
-
if (__DEV__) {
|
|
120
|
-
console.log("[useAuthAwarePurchase] No saved purchase to execute");
|
|
121
|
-
}
|
|
122
99
|
return false;
|
|
123
100
|
}
|
|
124
101
|
|
|
125
|
-
if (__DEV__) {
|
|
102
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
103
|
console.log("[useAuthAwarePurchase] Executing saved purchase:", saved.pkg.product.identifier);
|
|
127
104
|
}
|
|
128
105
|
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useCredits Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Auto-initializes free credits for
|
|
4
|
+
* Fetches user credits - NO CACHE, always fresh data.
|
|
5
|
+
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
|
+
* Auto-initializes free credits for registered users only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
10
10
|
import { useCallback, useMemo, useEffect } from "react";
|
|
11
|
+
import {
|
|
12
|
+
useAuthStore,
|
|
13
|
+
selectUserId,
|
|
14
|
+
selectIsAnonymous,
|
|
15
|
+
} from "@umituz/react-native-auth";
|
|
11
16
|
import type { UserCredits } from "../../domain/entities/Credits";
|
|
12
17
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
getCreditsRepository,
|
|
19
|
+
getCreditsConfig,
|
|
20
|
+
isCreditsRepositoryConfigured,
|
|
16
21
|
} from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
17
22
|
|
|
18
23
|
declare const __DEV__: boolean;
|
|
@@ -22,33 +27,8 @@ export const creditsQueryKeys = {
|
|
|
22
27
|
user: (userId: string) => ["credits", userId] as const,
|
|
23
28
|
};
|
|
24
29
|
|
|
25
|
-
/** Default stale time: 30 seconds - prevents infinite re-render loops */
|
|
26
|
-
const DEFAULT_STALE_TIME = 30 * 1000;
|
|
27
|
-
/** Default gc time: 5 minutes */
|
|
28
|
-
const DEFAULT_GC_TIME = 5 * 60 * 1000;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Global tracker for free credits initialization attempts.
|
|
32
|
-
* Shared across all useCredits hook instances to prevent multiple inits.
|
|
33
|
-
*/
|
|
34
30
|
const freeCreditsInitAttempted = new Set<string>();
|
|
35
31
|
|
|
36
|
-
export interface CreditsCacheConfig {
|
|
37
|
-
/** Time in ms before data is considered stale. Default: 30 seconds */
|
|
38
|
-
staleTime?: number;
|
|
39
|
-
/** Time in ms before inactive data is garbage collected. Default: 5 minutes */
|
|
40
|
-
gcTime?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface UseCreditsParams {
|
|
44
|
-
userId: string | undefined;
|
|
45
|
-
enabled?: boolean;
|
|
46
|
-
/** Whether user is anonymous. Anonymous users don't get free credits. */
|
|
47
|
-
isAnonymous?: boolean;
|
|
48
|
-
/** Cache configuration. Default: 30 second staleTime, 5 minute gcTime */
|
|
49
|
-
cache?: CreditsCacheConfig;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
32
|
export interface UseCreditsResult {
|
|
53
33
|
credits: UserCredits | null;
|
|
54
34
|
isLoading: boolean;
|
|
@@ -56,24 +36,18 @@ export interface UseCreditsResult {
|
|
|
56
36
|
hasCredits: boolean;
|
|
57
37
|
creditsPercent: number;
|
|
58
38
|
refetch: () => void;
|
|
59
|
-
/** Check if user can afford a specific credit cost */
|
|
60
39
|
canAfford: (cost: number) => boolean;
|
|
61
40
|
}
|
|
62
41
|
|
|
63
|
-
export const useCredits = ({
|
|
64
|
-
userId
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}: UseCreditsParams): UseCreditsResult => {
|
|
42
|
+
export const useCredits = (): UseCreditsResult => {
|
|
43
|
+
const userId = useAuthStore(selectUserId);
|
|
44
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
45
|
+
const isRegisteredUser = !!userId && !isAnonymous;
|
|
46
|
+
|
|
69
47
|
const isConfigured = isCreditsRepositoryConfigured();
|
|
70
48
|
const config = getCreditsConfig();
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
const staleTime = cache?.staleTime ?? DEFAULT_STALE_TIME;
|
|
74
|
-
const gcTime = cache?.gcTime ?? DEFAULT_GC_TIME;
|
|
75
|
-
|
|
76
|
-
const queryEnabled = enabled && !!userId && isConfigured;
|
|
50
|
+
const queryEnabled = !!userId && isConfigured;
|
|
77
51
|
|
|
78
52
|
const { data, isLoading, error, refetch, isFetched } = useQuery({
|
|
79
53
|
queryKey: creditsQueryKeys.user(userId ?? ""),
|
|
@@ -87,7 +61,6 @@ export const useCredits = ({
|
|
|
87
61
|
throw new Error(result.error?.message || "Failed to fetch credits");
|
|
88
62
|
}
|
|
89
63
|
|
|
90
|
-
// Background sync: If mapper detected expired status, sync to Firestore
|
|
91
64
|
if (result.data?.status === "expired") {
|
|
92
65
|
repository.syncExpiredStatus(userId).catch((syncError) => {
|
|
93
66
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -99,8 +72,8 @@ export const useCredits = ({
|
|
|
99
72
|
return result.data || null;
|
|
100
73
|
},
|
|
101
74
|
enabled: queryEnabled,
|
|
102
|
-
staleTime,
|
|
103
|
-
gcTime,
|
|
75
|
+
staleTime: 0,
|
|
76
|
+
gcTime: 0,
|
|
104
77
|
refetchOnMount: true,
|
|
105
78
|
refetchOnWindowFocus: true,
|
|
106
79
|
refetchOnReconnect: true,
|
|
@@ -108,33 +81,23 @@ export const useCredits = ({
|
|
|
108
81
|
|
|
109
82
|
const credits = data ?? null;
|
|
110
83
|
|
|
111
|
-
// Auto-initialize free credits for new users
|
|
112
84
|
const freeCredits = config.freeCredits ?? 0;
|
|
113
85
|
const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
|
|
114
86
|
|
|
115
87
|
useEffect(() => {
|
|
116
|
-
// Only run if:
|
|
117
|
-
// 1. Query has completed (isFetched)
|
|
118
|
-
// 2. User is authenticated (not anonymous)
|
|
119
|
-
// 3. No credits data exists
|
|
120
|
-
// 4. Free credits configured
|
|
121
|
-
// 5. Auto-init enabled
|
|
122
|
-
// 6. Haven't already attempted for this user (global tracking)
|
|
123
|
-
// 7. User is NOT anonymous (anonymous users must register first)
|
|
124
88
|
if (
|
|
125
89
|
isFetched &&
|
|
126
90
|
userId &&
|
|
127
|
-
|
|
91
|
+
isRegisteredUser &&
|
|
128
92
|
isConfigured &&
|
|
129
93
|
!credits &&
|
|
130
94
|
autoInit &&
|
|
131
95
|
!freeCreditsInitAttempted.has(userId)
|
|
132
96
|
) {
|
|
133
|
-
// Mark as attempted IMMEDIATELY to prevent other hook instances
|
|
134
97
|
freeCreditsInitAttempted.add(userId);
|
|
135
98
|
|
|
136
99
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
137
|
-
console.log("[useCredits]
|
|
100
|
+
console.log("[useCredits] Initializing free credits for registered user:", userId.slice(0, 8));
|
|
138
101
|
}
|
|
139
102
|
|
|
140
103
|
const repository = getCreditsRepository();
|
|
@@ -152,25 +115,20 @@ export const useCredits = ({
|
|
|
152
115
|
});
|
|
153
116
|
} else if (isFetched && userId && isAnonymous && !credits && autoInit) {
|
|
154
117
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
155
|
-
console.log("[useCredits] Skipping free credits
|
|
118
|
+
console.log("[useCredits] Skipping free credits - anonymous user must register first");
|
|
156
119
|
}
|
|
157
120
|
}
|
|
158
|
-
}, [isFetched, userId, isAnonymous, isConfigured, credits, autoInit, refetch]);
|
|
121
|
+
}, [isFetched, userId, isRegisteredUser, isAnonymous, isConfigured, credits, autoInit, refetch]);
|
|
159
122
|
|
|
160
|
-
// Memoize derived values to prevent unnecessary re-renders
|
|
161
123
|
const derivedValues = useMemo(() => {
|
|
162
124
|
const has = (credits?.credits ?? 0) > 0;
|
|
163
125
|
const percent = credits
|
|
164
126
|
? Math.round((credits.credits / config.creditLimit) * 100)
|
|
165
127
|
: 0;
|
|
166
128
|
|
|
167
|
-
return {
|
|
168
|
-
hasCredits: has,
|
|
169
|
-
creditsPercent: percent,
|
|
170
|
-
};
|
|
129
|
+
return { hasCredits: has, creditsPercent: percent };
|
|
171
130
|
}, [credits, config.creditLimit]);
|
|
172
131
|
|
|
173
|
-
// Memoize canAfford to prevent recreation on every render
|
|
174
132
|
const canAfford = useCallback(
|
|
175
133
|
(cost: number): boolean => {
|
|
176
134
|
if (!credits) return false;
|
|
@@ -190,11 +148,7 @@ export const useCredits = ({
|
|
|
190
148
|
};
|
|
191
149
|
};
|
|
192
150
|
|
|
193
|
-
export const useHasCredits = (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
): boolean => {
|
|
197
|
-
const { credits } = useCredits({ userId, isAnonymous });
|
|
198
|
-
if (!credits) return false;
|
|
199
|
-
return credits.credits > 0;
|
|
151
|
+
export const useHasCredits = (): boolean => {
|
|
152
|
+
const { hasCredits } = useCredits();
|
|
153
|
+
return hasCredits;
|
|
200
154
|
};
|
|
@@ -1,101 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* usePremium Hook
|
|
3
|
-
* Complete subscription management for 100+ apps
|
|
4
|
-
* Works for both authenticated and anonymous users
|
|
5
3
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Complete subscription management.
|
|
5
|
+
* Auth info automatically read from @umituz/react-native-auth.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
import { useCallback } from 'react';
|
|
11
9
|
import type { PurchasesPackage } from 'react-native-purchases';
|
|
10
|
+
import {
|
|
11
|
+
useAuthStore,
|
|
12
|
+
selectUserId,
|
|
13
|
+
} from "@umituz/react-native-auth";
|
|
12
14
|
import type { UserCredits } from '../../domain/entities/Credits';
|
|
13
15
|
import { useCredits } from './useCredits';
|
|
14
16
|
import { useSubscriptionStatus } from './useSubscriptionStatus';
|
|
15
17
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
useSubscriptionPackages,
|
|
19
|
+
usePurchasePackage,
|
|
20
|
+
useRestorePurchase,
|
|
19
21
|
} from '../../revenuecat/presentation/hooks/useSubscriptionQueries';
|
|
20
22
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
21
23
|
|
|
24
|
+
declare const __DEV__: boolean;
|
|
25
|
+
|
|
22
26
|
export interface UsePremiumResult {
|
|
23
|
-
/** User has active premium subscription */
|
|
24
27
|
isPremium: boolean;
|
|
25
|
-
/** Loading credits or packages */
|
|
26
28
|
isLoading: boolean;
|
|
27
|
-
/** Available subscription packages */
|
|
28
29
|
packages: PurchasesPackage[];
|
|
29
|
-
/** User's credits (null if not premium) */
|
|
30
30
|
credits: UserCredits | null;
|
|
31
|
-
/** Paywall visibility state */
|
|
32
31
|
showPaywall: boolean;
|
|
33
|
-
/** Purchase a subscription package */
|
|
34
32
|
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
35
|
-
/** Restore previous purchases */
|
|
36
33
|
restorePurchase: () => Promise<boolean>;
|
|
37
|
-
/** Set paywall visibility */
|
|
38
34
|
setShowPaywall: (show: boolean) => void;
|
|
39
|
-
/** Close paywall */
|
|
40
35
|
closePaywall: () => void;
|
|
41
|
-
/** Open paywall */
|
|
42
36
|
openPaywall: () => void;
|
|
43
37
|
}
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*
|
|
48
|
-
* @param userId - User ID (undefined for anonymous users)
|
|
49
|
-
* @returns Premium status, packages, and subscription actions
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```typescript
|
|
53
|
-
* const { isPremium, packages, purchasePackage } = usePremium(userId);
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export const usePremium = (userId?: string): UsePremiumResult => {
|
|
57
|
-
// Fetch real subscription status from RevenueCat
|
|
58
|
-
const { isPremium: subscriptionActive, isLoading: statusLoading } =
|
|
59
|
-
useSubscriptionStatus({
|
|
60
|
-
userId,
|
|
61
|
-
enabled: !!userId,
|
|
62
|
-
});
|
|
39
|
+
export const usePremium = (): UsePremiumResult => {
|
|
40
|
+
const userId = useAuthStore(selectUserId);
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
const { credits, isLoading: creditsLoading } = useCredits(
|
|
66
|
-
userId,
|
|
67
|
-
enabled: !!userId,
|
|
68
|
-
});
|
|
42
|
+
const { isPremium: subscriptionActive, isLoading: statusLoading } = useSubscriptionStatus();
|
|
43
|
+
const { credits, isLoading: creditsLoading } = useCredits();
|
|
69
44
|
|
|
70
|
-
|
|
71
|
-
const { data: packages = [], isLoading: packagesLoading } =
|
|
72
|
-
useSubscriptionPackages(userId);
|
|
45
|
+
const { data: packages = [], isLoading: packagesLoading } = useSubscriptionPackages(userId ?? undefined);
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const restoreMutation = useRestorePurchase(userId);
|
|
47
|
+
const purchaseMutation = usePurchasePackage(userId ?? undefined);
|
|
48
|
+
const restoreMutation = useRestorePurchase(userId ?? undefined);
|
|
77
49
|
|
|
78
|
-
|
|
79
|
-
const { showPaywall, setShowPaywall, closePaywall, openPaywall } =
|
|
80
|
-
usePaywallVisibility();
|
|
50
|
+
const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
|
|
81
51
|
|
|
82
|
-
// Premium status = actual subscription status from RevenueCat
|
|
83
52
|
const isPremium = subscriptionActive;
|
|
84
53
|
|
|
85
|
-
// Purchase handler with proper error handling
|
|
86
54
|
const handlePurchase = useCallback(
|
|
87
55
|
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log("[usePremium] handlePurchase
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
console.log("[usePremium] handlePurchase:", pkg.product.identifier);
|
|
90
58
|
}
|
|
91
59
|
try {
|
|
92
60
|
const result = await purchaseMutation.mutateAsync(pkg);
|
|
93
|
-
if (__DEV__) {
|
|
94
|
-
console.log("[usePremium] Purchase result:", { success: result.success });
|
|
95
|
-
}
|
|
96
61
|
return result.success;
|
|
97
62
|
} catch (error) {
|
|
98
|
-
if (__DEV__) {
|
|
63
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
99
64
|
console.error("[usePremium] Purchase failed:", error);
|
|
100
65
|
}
|
|
101
66
|
return false;
|
|
@@ -104,13 +69,12 @@ export const usePremium = (userId?: string): UsePremiumResult => {
|
|
|
104
69
|
[purchaseMutation],
|
|
105
70
|
);
|
|
106
71
|
|
|
107
|
-
// Restore handler with proper error handling
|
|
108
72
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
109
73
|
try {
|
|
110
74
|
const result = await restoreMutation.mutateAsync();
|
|
111
75
|
return result.success;
|
|
112
76
|
} catch (error) {
|
|
113
|
-
if (__DEV__) {
|
|
77
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
114
78
|
console.error("[usePremium] Restore failed:", error);
|
|
115
79
|
}
|
|
116
80
|
return false;
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
7
|
+
import {
|
|
8
|
+
useAuthStore,
|
|
9
|
+
selectUserId,
|
|
10
|
+
selectIsAnonymous,
|
|
11
|
+
} from "@umituz/react-native-auth";
|
|
7
12
|
import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
|
|
8
13
|
import { usePremium } from "./usePremium";
|
|
9
14
|
import { SubscriptionManager } from "../../revenuecat";
|
|
@@ -12,8 +17,6 @@ import { usePurchaseLoadingStore } from "../stores";
|
|
|
12
17
|
declare const __DEV__: boolean;
|
|
13
18
|
|
|
14
19
|
export interface UseSavedPurchaseAutoExecutionParams {
|
|
15
|
-
userId?: string;
|
|
16
|
-
isAnonymous?: boolean;
|
|
17
20
|
onSuccess?: () => void;
|
|
18
21
|
onError?: (error: Error) => void;
|
|
19
22
|
}
|
|
@@ -23,24 +26,26 @@ export interface UseSavedPurchaseAutoExecutionResult {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export const useSavedPurchaseAutoExecution = (
|
|
26
|
-
params
|
|
29
|
+
params?: UseSavedPurchaseAutoExecutionParams
|
|
27
30
|
): UseSavedPurchaseAutoExecutionResult => {
|
|
28
|
-
const {
|
|
29
|
-
|
|
31
|
+
const { onSuccess, onError } = params ?? {};
|
|
32
|
+
|
|
33
|
+
const userId = useAuthStore(selectUserId);
|
|
34
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
35
|
+
|
|
36
|
+
const { purchasePackage } = usePremium();
|
|
30
37
|
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
31
38
|
|
|
32
39
|
const prevIsAnonymousRef = useRef<boolean | undefined>(undefined);
|
|
33
40
|
const isExecutingRef = useRef(false);
|
|
34
41
|
const hasExecutedRef = useRef(false);
|
|
35
42
|
|
|
36
|
-
// Store callbacks in refs to avoid dependency changes
|
|
37
43
|
const purchasePackageRef = useRef(purchasePackage);
|
|
38
44
|
const onSuccessRef = useRef(onSuccess);
|
|
39
45
|
const onErrorRef = useRef(onError);
|
|
40
46
|
const startPurchaseRef = useRef(startPurchase);
|
|
41
47
|
const endPurchaseRef = useRef(endPurchase);
|
|
42
48
|
|
|
43
|
-
// Update refs when values change
|
|
44
49
|
useEffect(() => {
|
|
45
50
|
purchasePackageRef.current = purchasePackage;
|
|
46
51
|
}, [purchasePackage]);
|
|
@@ -69,10 +74,9 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
69
74
|
const wasAnonymous = prevIsAnonymous === true;
|
|
70
75
|
const becameAuthenticated = wasAnonymous && isAuthenticated;
|
|
71
76
|
|
|
72
|
-
// Only log when there's a state change worth noting
|
|
73
77
|
const shouldLog = prevIsAnonymousRef.current !== isAnonymous;
|
|
74
78
|
|
|
75
|
-
if (__DEV__ && shouldLog) {
|
|
79
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && shouldLog) {
|
|
76
80
|
console.log("[SavedPurchaseAutoExecution] Auth state check:", {
|
|
77
81
|
userId: userId?.slice(0, 8),
|
|
78
82
|
prevIsAnonymous,
|
|
@@ -90,21 +94,15 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
// Execute only once when transitioning from anonymous to authenticated
|
|
94
97
|
if (
|
|
95
98
|
becameAuthenticated &&
|
|
96
99
|
savedPurchase &&
|
|
97
100
|
!isExecutingRef.current &&
|
|
98
101
|
!hasExecutedRef.current
|
|
99
102
|
) {
|
|
100
|
-
if (__DEV__) {
|
|
101
|
-
console.log("[SavedPurchaseAutoExecution] Triggering auto-execution...");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
103
|
hasExecutedRef.current = true;
|
|
105
104
|
isExecutingRef.current = true;
|
|
106
105
|
|
|
107
|
-
// Execute purchase flow
|
|
108
106
|
const executeFlow = async () => {
|
|
109
107
|
const currentUserId = userId;
|
|
110
108
|
if (!currentUserId) {
|
|
@@ -112,75 +110,31 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
112
110
|
return;
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
if (__DEV__) {
|
|
116
|
-
console.log(
|
|
117
|
-
"[SavedPurchaseAutoExecution] Waiting for RevenueCat initialization...",
|
|
118
|
-
{
|
|
119
|
-
userId: currentUserId.slice(0, 8),
|
|
120
|
-
productId: savedPurchase.pkg.product.identifier,
|
|
121
|
-
}
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
113
|
const maxAttempts = 20;
|
|
126
114
|
const delayMs = 500;
|
|
127
115
|
|
|
128
116
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
129
117
|
const isReady = SubscriptionManager.isInitializedForUser(currentUserId);
|
|
130
118
|
|
|
131
|
-
if (__DEV__) {
|
|
132
|
-
console.log(
|
|
133
|
-
`[SavedPurchaseAutoExecution] Attempt ${attempt + 1}/${maxAttempts}, isReady: ${isReady}`
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
119
|
if (isReady) {
|
|
138
120
|
const pkg = savedPurchase.pkg;
|
|
139
121
|
clearSavedPurchase();
|
|
140
122
|
|
|
141
|
-
if (__DEV__) {
|
|
142
|
-
console.log(
|
|
143
|
-
"[SavedPurchaseAutoExecution] RevenueCat ready, starting purchase...",
|
|
144
|
-
{ productId: pkg.product.identifier, userId: currentUserId.slice(0, 8) }
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
123
|
startPurchaseRef.current(pkg.product.identifier, "auto-execution");
|
|
149
124
|
|
|
150
125
|
try {
|
|
151
|
-
if (__DEV__) {
|
|
152
|
-
console.log("[SavedPurchaseAutoExecution] Calling purchasePackage...");
|
|
153
|
-
}
|
|
154
|
-
|
|
155
126
|
const success = await purchasePackageRef.current(pkg);
|
|
156
127
|
|
|
157
|
-
if (__DEV__) {
|
|
158
|
-
console.log("[SavedPurchaseAutoExecution] Purchase completed:", {
|
|
159
|
-
success,
|
|
160
|
-
productId: pkg.product.identifier,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
128
|
if (success && onSuccessRef.current) {
|
|
165
129
|
onSuccessRef.current();
|
|
166
130
|
}
|
|
167
131
|
} catch (error) {
|
|
168
|
-
if (__DEV__) {
|
|
169
|
-
console.error("[SavedPurchaseAutoExecution] Purchase error:", {
|
|
170
|
-
error,
|
|
171
|
-
productId: pkg.product.identifier,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
132
|
if (onErrorRef.current && error instanceof Error) {
|
|
175
133
|
onErrorRef.current(error);
|
|
176
134
|
}
|
|
177
135
|
} finally {
|
|
178
136
|
endPurchaseRef.current();
|
|
179
137
|
isExecutingRef.current = false;
|
|
180
|
-
|
|
181
|
-
if (__DEV__) {
|
|
182
|
-
console.log("[SavedPurchaseAutoExecution] Purchase flow finished");
|
|
183
|
-
}
|
|
184
138
|
}
|
|
185
139
|
|
|
186
140
|
return;
|
|
@@ -189,11 +143,6 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
189
143
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
190
144
|
}
|
|
191
145
|
|
|
192
|
-
if (__DEV__) {
|
|
193
|
-
console.log(
|
|
194
|
-
"[SavedPurchaseAutoExecution] Timeout waiting for RevenueCat, clearing saved purchase"
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
146
|
clearSavedPurchase();
|
|
198
147
|
isExecutingRef.current = false;
|
|
199
148
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useSubscriptionSettingsConfig Hook
|
|
3
3
|
* Returns ready-to-use config for settings screens
|
|
4
|
-
* Single Source of Truth: Firestore (credits document)
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { useMemo, useCallback } from "react";
|
|
@@ -24,58 +23,43 @@ export type {
|
|
|
24
23
|
UseSubscriptionSettingsConfigParams,
|
|
25
24
|
} from "../types/SubscriptionSettingsTypes";
|
|
26
25
|
|
|
27
|
-
/**
|
|
28
|
-
* Hook that returns ready-to-use subscription config for settings
|
|
29
|
-
* Single Source of Truth: Firestore credits document
|
|
30
|
-
*/
|
|
31
26
|
export const useSubscriptionSettingsConfig = (
|
|
32
|
-
params: UseSubscriptionSettingsConfigParams
|
|
27
|
+
params: Omit<UseSubscriptionSettingsConfigParams, 'userId'>
|
|
33
28
|
): SubscriptionSettingsConfig => {
|
|
34
|
-
const {
|
|
29
|
+
const { translations, creditLimit, upgradePrompt } = params;
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
const { credits } = useCredits({ userId, enabled: !!userId });
|
|
31
|
+
const { credits } = useCredits();
|
|
38
32
|
const { openPaywall } = usePaywallVisibility();
|
|
39
33
|
|
|
40
34
|
const handleOpenPaywall = useCallback(() => {
|
|
41
35
|
openPaywall("settings");
|
|
42
36
|
}, [openPaywall]);
|
|
43
37
|
|
|
44
|
-
// All data from Firestore (Single Source of Truth)
|
|
45
38
|
const isPremium = credits?.isPremium ?? false;
|
|
46
39
|
const willRenew = credits?.willRenew ?? false;
|
|
47
40
|
|
|
48
|
-
// Expiration date from Firestore
|
|
49
41
|
const expiresAtIso = credits?.expirationDate?.toISOString() ?? null;
|
|
50
|
-
|
|
51
|
-
// Purchase date from Firestore
|
|
52
42
|
const purchasedAtIso = credits?.purchasedAt?.toISOString() ?? null;
|
|
53
43
|
|
|
54
|
-
// Credit limit from Firestore or config fallback
|
|
55
44
|
const dynamicCreditLimit = useMemo(() => {
|
|
56
45
|
if (credits?.creditLimit) return credits.creditLimit;
|
|
57
46
|
const config = getCreditsConfig();
|
|
58
47
|
return creditLimit ?? config.creditLimit;
|
|
59
48
|
}, [credits?.creditLimit, creditLimit]);
|
|
60
49
|
|
|
61
|
-
// Formatted dates
|
|
62
50
|
const formattedExpirationDate = useMemo(() => formatDate(expiresAtIso), [expiresAtIso]);
|
|
63
51
|
const formattedPurchaseDate = useMemo(() => formatDate(purchasedAtIso), [purchasedAtIso]);
|
|
64
52
|
|
|
65
|
-
// Days remaining
|
|
66
53
|
const daysRemaining = useMemo(() => calculateDaysRemaining(expiresAtIso), [expiresAtIso]);
|
|
67
54
|
|
|
68
|
-
// Period type from Firestore
|
|
69
55
|
const periodType = credits?.periodType;
|
|
70
56
|
|
|
71
|
-
// Status type: prioritize Firestore status, then derive from willRenew + expiration + periodType
|
|
72
57
|
const statusType: SubscriptionStatusType = credits?.status
|
|
73
58
|
? (credits.status as SubscriptionStatusType)
|
|
74
59
|
: getSubscriptionStatusType(isPremium, willRenew, expiresAtIso, periodType);
|
|
75
60
|
|
|
76
61
|
const creditsArray = useCreditsArray(credits, dynamicCreditLimit, translations);
|
|
77
62
|
|
|
78
|
-
// Centralized display flags
|
|
79
63
|
const hasCredits = creditsArray.length > 0;
|
|
80
64
|
const display = useMemo(() => ({
|
|
81
65
|
showHeader: isPremium || hasCredits,
|
|
@@ -84,7 +68,6 @@ export const useSubscriptionSettingsConfig = (
|
|
|
84
68
|
showExpirationDate: (isPremium || hasCredits) && !!expiresAtIso,
|
|
85
69
|
}), [isPremium, hasCredits, upgradePrompt, expiresAtIso]);
|
|
86
70
|
|
|
87
|
-
// Build config
|
|
88
71
|
return useMemo((): SubscriptionSettingsConfig => ({
|
|
89
72
|
enabled: true,
|
|
90
73
|
settingsItem: {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useSubscriptionStatus Hook
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Checks real subscription status from RevenueCat.
|
|
5
|
+
* Auth info automatically read from @umituz/react-native-auth.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
9
|
+
import {
|
|
10
|
+
useAuthStore,
|
|
11
|
+
selectUserId,
|
|
12
|
+
} from "@umituz/react-native-auth";
|
|
9
13
|
import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
|
|
10
14
|
|
|
11
15
|
export const subscriptionStatusQueryKeys = {
|
|
@@ -21,34 +25,20 @@ export interface SubscriptionStatusResult {
|
|
|
21
25
|
refetch: () => void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
export
|
|
25
|
-
userId
|
|
26
|
-
enabled?: boolean;
|
|
27
|
-
}
|
|
28
|
+
export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
29
|
+
const userId = useAuthStore(selectUserId);
|
|
28
30
|
|
|
29
|
-
/**
|
|
30
|
-
* Check real subscription status from RevenueCat
|
|
31
|
-
*
|
|
32
|
-
* @param userId - User ID
|
|
33
|
-
* @param enabled - Whether to enable the query
|
|
34
|
-
* @returns Subscription status with isPremium flag
|
|
35
|
-
*/
|
|
36
|
-
export const useSubscriptionStatus = ({
|
|
37
|
-
userId,
|
|
38
|
-
enabled = true,
|
|
39
|
-
}: UseSubscriptionStatusParams): SubscriptionStatusResult => {
|
|
40
31
|
const { data, isLoading, error, refetch } = useQuery({
|
|
41
32
|
queryKey: subscriptionStatusQueryKeys.user(userId ?? ""),
|
|
42
33
|
queryFn: async () => {
|
|
43
34
|
if (!userId) {
|
|
44
35
|
return { isPremium: false, expirationDate: null };
|
|
45
36
|
}
|
|
46
|
-
|
|
47
37
|
return SubscriptionManager.checkPremiumStatus();
|
|
48
38
|
},
|
|
49
|
-
enabled:
|
|
50
|
-
staleTime: 0,
|
|
51
|
-
gcTime: 0,
|
|
39
|
+
enabled: !!userId && SubscriptionManager.isInitializedForUser(userId),
|
|
40
|
+
staleTime: 0,
|
|
41
|
+
gcTime: 0,
|
|
52
42
|
refetchOnMount: true,
|
|
53
43
|
refetchOnWindowFocus: true,
|
|
54
44
|
refetchOnReconnect: true,
|