@umituz/react-native-subscription 2.35.15 → 2.35.17
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/config/utils/planSelectors.ts +5 -1
- package/src/domains/credits/presentation/useCredits.ts +6 -5
- package/src/domains/paywall/hooks/usePaywallActions.ts +2 -82
- package/src/domains/revenuecat/core/customerInfoHelpers.ts +21 -0
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +0 -19
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +1 -1
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +2 -8
- package/src/domains/subscription/application/initializer/ConfigValidator.ts +2 -2
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +23 -3
- package/src/domains/subscription/application/statusChangeHandlers.ts +0 -30
- package/src/domains/subscription/constants/thresholds.ts +10 -0
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +3 -3
- package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +0 -19
- package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +1 -1
- package/src/domains/subscription/infrastructure/hooks/useInitializeSubscription.ts +2 -4
- package/src/domains/subscription/infrastructure/hooks/usePaywallFlow.ts +12 -2
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -44
- package/src/domains/subscription/infrastructure/hooks/useRevenueCat.ts +23 -5
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +1 -31
- package/src/domains/subscription/infrastructure/services/OfferingsFetcher.ts +0 -21
- package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +6 -36
- package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +0 -6
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -44
- package/src/domains/subscription/presentation/featureGateActions.ts +0 -37
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +1 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +1 -1
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +0 -43
- package/src/domains/subscription/presentation/useFeatureGate.ts +1 -40
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +6 -20
- package/src/domains/subscription/utils/authGuards.ts +26 -2
- package/src/domains/subscription/utils/expirationHelpers.ts +2 -2
- package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
- package/src/domains/trial/application/TrialService.ts +12 -4
- package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +1 -1
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +2 -5
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +2 -5
- package/src/shared/infrastructure/react-query/hooks/usePreviousUserCleanup.ts +39 -0
- package/src/shared/infrastructure/react-query/queryConfig.ts +22 -0
- package/src/shared/infrastructure/react-query/queryInvalidation.ts +46 -0
- package/src/shared/presentation/hooks/useServiceCall.ts +2 -1
- package/src/shared/utils/errorUtils.ts +32 -0
- package/src/utils/appUtils.ts +6 -0
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.constants.ts +0 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.constants.ts +0 -1
|
@@ -6,8 +6,6 @@ import { executeFeatureAction } from "./featureGateActions";
|
|
|
6
6
|
|
|
7
7
|
export type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
|
|
8
8
|
|
|
9
|
-
declare const __DEV__: boolean;
|
|
10
|
-
|
|
11
9
|
export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
|
|
12
10
|
const {
|
|
13
11
|
isAuthenticated,
|
|
@@ -27,16 +25,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
27
25
|
const { creditBalanceRef, hasSubscriptionRef, onShowPaywallRef, requiredCreditsRef, isCreditsLoadedRef } = useSyncedRefs(creditBalance, hasSubscription, onShowPaywall, requiredCredits, isCreditsLoaded);
|
|
28
26
|
|
|
29
27
|
useEffect(() => {
|
|
30
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
31
|
-
console.log("[FeatureGate] Auth completion useEffect triggered:", {
|
|
32
|
-
isWaitingForAuthCredits: isWaitingForAuthCreditsRef.current,
|
|
33
|
-
isCreditsLoaded,
|
|
34
|
-
hasPendingAction: !!pendingActionRef.current,
|
|
35
|
-
hasSubscription,
|
|
36
|
-
creditBalance,
|
|
37
|
-
requiredCredits,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
28
|
|
|
41
29
|
const shouldExecute = canExecuteAuthAction(
|
|
42
30
|
isWaitingForAuthCreditsRef.current,
|
|
@@ -47,14 +35,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
47
35
|
requiredCredits
|
|
48
36
|
);
|
|
49
37
|
|
|
50
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
|
-
console.log("[FeatureGate] canExecuteAuthAction:", shouldExecute);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
38
|
if (shouldExecute) {
|
|
55
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
56
|
-
console.log("[FeatureGate] ✅ EXECUTING PENDING ACTION after auth!");
|
|
57
|
-
}
|
|
58
39
|
isWaitingForAuthCreditsRef.current = false;
|
|
59
40
|
const action = pendingActionRef.current!;
|
|
60
41
|
pendingActionRef.current = null;
|
|
@@ -63,9 +44,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
63
44
|
}
|
|
64
45
|
|
|
65
46
|
if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
|
|
66
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
-
console.log("[FeatureGate] Auth credits loaded but insufficient, showing paywall");
|
|
68
|
-
}
|
|
69
47
|
isWaitingForAuthCreditsRef.current = false;
|
|
70
48
|
isWaitingForPurchaseRef.current = true;
|
|
71
49
|
onShowPaywall(requiredCredits);
|
|
@@ -73,16 +51,6 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
73
51
|
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywall]);
|
|
74
52
|
|
|
75
53
|
useEffect(() => {
|
|
76
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
77
|
-
console.log("[FeatureGate] Purchase completion useEffect triggered:", {
|
|
78
|
-
creditBalance,
|
|
79
|
-
prevCreditBalance: prevCreditBalanceRef.current,
|
|
80
|
-
hasSubscription,
|
|
81
|
-
prevHasSubscription: hasSubscriptionRef.current,
|
|
82
|
-
isWaitingForPurchase: isWaitingForPurchaseRef.current,
|
|
83
|
-
hasPendingAction: !!pendingActionRef.current,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
54
|
|
|
87
55
|
const shouldExecute = canExecutePurchaseAction(
|
|
88
56
|
isWaitingForPurchaseRef.current,
|
|
@@ -93,14 +61,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
93
61
|
!!pendingActionRef.current
|
|
94
62
|
);
|
|
95
63
|
|
|
96
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
-
console.log("[FeatureGate] canExecutePurchaseAction:", shouldExecute);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
64
|
if (shouldExecute) {
|
|
101
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
-
console.log("[FeatureGate] ✅ EXECUTING PENDING ACTION after purchase!");
|
|
103
|
-
}
|
|
104
65
|
const action = pendingActionRef.current!;
|
|
105
66
|
pendingActionRef.current = null;
|
|
106
67
|
isWaitingForPurchaseRef.current = false;
|
|
@@ -108,7 +69,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
108
69
|
}
|
|
109
70
|
|
|
110
71
|
prevCreditBalanceRef.current = creditBalance;
|
|
111
|
-
hasSubscriptionRef
|
|
72
|
+
// hasSubscriptionRef is already synced by useSyncedRefs, no need to update manually
|
|
112
73
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
74
|
}, [creditBalance, hasSubscription]);
|
|
114
75
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
|
-
import { useEffect
|
|
2
|
+
import { useEffect } 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";
|
|
6
6
|
import { SubscriptionStatusResult } from "./useSubscriptionStatus.types";
|
|
7
7
|
import { isAuthenticated } from "../utils/authGuards";
|
|
8
|
+
import { NO_CACHE_QUERY_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
|
|
9
|
+
import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
|
|
8
10
|
|
|
9
11
|
export const subscriptionStatusQueryKeys = {
|
|
10
12
|
all: ["subscriptionStatus"] as const,
|
|
@@ -38,27 +40,11 @@ export const useSubscriptionStatus = (): SubscriptionStatusResult => {
|
|
|
38
40
|
}
|
|
39
41
|
},
|
|
40
42
|
enabled: queryEnabled,
|
|
41
|
-
|
|
42
|
-
staleTime: 0,
|
|
43
|
-
refetchOnMount: "always",
|
|
44
|
-
refetchOnWindowFocus: "always",
|
|
45
|
-
refetchOnReconnect: "always",
|
|
43
|
+
...NO_CACHE_QUERY_CONFIG,
|
|
46
44
|
});
|
|
47
45
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
const prevUserId = prevUserIdRef.current;
|
|
53
|
-
prevUserIdRef.current = userId;
|
|
54
|
-
|
|
55
|
-
// Clear previous user's cache when userId changes (logout or user switch)
|
|
56
|
-
if (prevUserId !== userId && isAuthenticated(prevUserId)) {
|
|
57
|
-
queryClient.removeQueries({
|
|
58
|
-
queryKey: subscriptionStatusQueryKeys.user(prevUserId),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}, [userId, queryClient]);
|
|
46
|
+
// Clean up previous user's cache on logout/user switch
|
|
47
|
+
usePreviousUserCleanup(userId, queryClient, subscriptionStatusQueryKeys.user);
|
|
62
48
|
|
|
63
49
|
useEffect(() => {
|
|
64
50
|
if (!isAuthenticated(userId)) return undefined;
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { isDefined } from "../../../shared/utils/validators";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export function isAuthenticated(userId: string | null | undefined): userId is string {
|
|
4
4
|
return isDefined(userId) && userId.length > 0;
|
|
5
|
-
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Requires user to be authenticated, throws if not
|
|
9
|
+
* Type guard that asserts userId is string
|
|
10
|
+
*
|
|
11
|
+
* @param userId - User ID to check
|
|
12
|
+
* @param errorMessage - Custom error message (optional)
|
|
13
|
+
* @throws Error if user is not authenticated
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* function purchaseProduct(userId: string | null) {
|
|
17
|
+
* requireAuthentication(userId); // throws if null/undefined
|
|
18
|
+
* // TypeScript now knows userId is string
|
|
19
|
+
* await purchase(userId);
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export function requireAuthentication(
|
|
23
|
+
userId: string | null | undefined,
|
|
24
|
+
errorMessage = "User not authenticated"
|
|
25
|
+
): asserts userId is string {
|
|
26
|
+
if (!isAuthenticated(userId)) {
|
|
27
|
+
throw new Error(errorMessage);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { EXPIRATION_WARNING_THRESHOLD_DAYS } from '../constants/thresholds';
|
|
2
2
|
|
|
3
3
|
export function shouldHighlightExpiration(daysRemaining: number | null | undefined): boolean {
|
|
4
|
-
return daysRemaining !== null && daysRemaining !== undefined && daysRemaining > 0 && daysRemaining <=
|
|
4
|
+
return daysRemaining !== null && daysRemaining !== undefined && daysRemaining > 0 && daysRemaining <= EXPIRATION_WARNING_THRESHOLD_DAYS;
|
|
5
5
|
}
|
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
79
|
+
const id = await ensureDeviceId(deviceId);
|
|
72
80
|
const record: TrialRecordWrite = {
|
|
73
81
|
hasUsedTrial: true,
|
|
74
82
|
trialInProgress: false,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
|
+
import { NO_CACHE_QUERY_CONFIG } from "../../../../shared/infrastructure/react-query/queryConfig";
|
|
3
4
|
import type {
|
|
4
5
|
ProductMetadata,
|
|
5
6
|
ProductMetadataConfig,
|
|
@@ -50,11 +51,7 @@ export function useProductMetadata({
|
|
|
50
51
|
return service.getAll();
|
|
51
52
|
},
|
|
52
53
|
enabled,
|
|
53
|
-
|
|
54
|
-
staleTime: 0,
|
|
55
|
-
refetchOnMount: "always",
|
|
56
|
-
refetchOnWindowFocus: "always",
|
|
57
|
-
refetchOnReconnect: "always",
|
|
54
|
+
...NO_CACHE_QUERY_CONFIG,
|
|
58
55
|
});
|
|
59
56
|
|
|
60
57
|
const products = data ?? [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useQuery } from "@umituz/react-native-design-system";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
4
|
+
import { NO_CACHE_QUERY_CONFIG } from "../../../../shared/infrastructure/react-query/queryConfig";
|
|
4
5
|
import type {
|
|
5
6
|
CreditLog,
|
|
6
7
|
TransactionRepositoryConfig,
|
|
@@ -53,11 +54,7 @@ export function useTransactionHistory({
|
|
|
53
54
|
return result.data ?? [];
|
|
54
55
|
},
|
|
55
56
|
enabled: !!userId,
|
|
56
|
-
|
|
57
|
-
staleTime: 0,
|
|
58
|
-
refetchOnMount: "always",
|
|
59
|
-
refetchOnWindowFocus: "always",
|
|
60
|
-
refetchOnReconnect: "always",
|
|
57
|
+
...NO_CACHE_QUERY_CONFIG,
|
|
61
58
|
});
|
|
62
59
|
|
|
63
60
|
const transactions = data ?? [];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Cache Cleanup Hook
|
|
3
|
+
* Automatically cleans up previous user's query cache when userId changes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useRef } from "react";
|
|
7
|
+
import type { QueryClient } from "@umituz/react-native-design-system";
|
|
8
|
+
import { isAuthenticated } from "../../../../domains/subscription/utils/authGuards";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Cleans up previous user's cache when userId changes (logout or user switch)
|
|
12
|
+
* Prevents data leakage between users
|
|
13
|
+
*
|
|
14
|
+
* @param userId - Current user ID
|
|
15
|
+
* @param queryClient - TanStack Query client
|
|
16
|
+
* @param queryKey - Query key factory function that takes userId
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* usePreviousUserCleanup(userId, queryClient, (id) => creditsQueryKeys.user(id));
|
|
20
|
+
*/
|
|
21
|
+
export function usePreviousUserCleanup(
|
|
22
|
+
userId: string | null | undefined,
|
|
23
|
+
queryClient: QueryClient,
|
|
24
|
+
queryKey: (userId: string) => readonly unknown[]
|
|
25
|
+
): void {
|
|
26
|
+
const prevUserIdRef = useRef(userId);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const prevUserId = prevUserIdRef.current;
|
|
30
|
+
prevUserIdRef.current = userId;
|
|
31
|
+
|
|
32
|
+
// Clear previous user's cache when userId changes (logout or user switch)
|
|
33
|
+
if (prevUserId !== userId && isAuthenticated(prevUserId)) {
|
|
34
|
+
queryClient.removeQueries({
|
|
35
|
+
queryKey: queryKey(prevUserId),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}, [userId, queryClient, queryKey]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared TanStack Query Configuration
|
|
3
|
+
* Common query configurations to ensure consistency across hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration for queries that should never cache
|
|
8
|
+
* Used for real-time sensitive data (subscriptions, credits, transactions)
|
|
9
|
+
*
|
|
10
|
+
* - gcTime: 0 - Don't keep unused data in memory
|
|
11
|
+
* - staleTime: 0 - Always consider data stale
|
|
12
|
+
* - refetchOnMount: "always" - Always refetch when component mounts
|
|
13
|
+
* - refetchOnWindowFocus: "always" - Always refetch when window regains focus
|
|
14
|
+
* - refetchOnReconnect: "always" - Always refetch when reconnecting
|
|
15
|
+
*/
|
|
16
|
+
export const NO_CACHE_QUERY_CONFIG = {
|
|
17
|
+
gcTime: 0,
|
|
18
|
+
staleTime: 0,
|
|
19
|
+
refetchOnMount: "always" as const,
|
|
20
|
+
refetchOnWindowFocus: "always" as const,
|
|
21
|
+
refetchOnReconnect: "always" as const,
|
|
22
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Invalidation Utilities
|
|
3
|
+
* Centralized functions for invalidating multiple related queries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { QueryClient } from "@umituz/react-native-design-system";
|
|
7
|
+
import { creditsQueryKeys } from "../../../domains/credits/presentation/creditsQueryKeys";
|
|
8
|
+
import { subscriptionStatusQueryKeys } from "../../../domains/subscription/presentation/useSubscriptionStatus";
|
|
9
|
+
|
|
10
|
+
// Subscription packages query key
|
|
11
|
+
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
12
|
+
packages: ["subscription", "packages"] as const,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Invalidates all subscription-related queries
|
|
17
|
+
* Use after purchases, restores, or subscription changes
|
|
18
|
+
*
|
|
19
|
+
* @param queryClient - TanStack Query client
|
|
20
|
+
* @param userId - Optional user ID to invalidate user-specific queries
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // After successful purchase
|
|
24
|
+
* await invalidateSubscriptionQueries(queryClient, userId);
|
|
25
|
+
*/
|
|
26
|
+
export async function invalidateSubscriptionQueries(
|
|
27
|
+
queryClient: QueryClient,
|
|
28
|
+
userId?: string | null
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
// Invalidate packages (affects all users)
|
|
31
|
+
await queryClient.invalidateQueries({
|
|
32
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Invalidate user-specific queries if userId provided
|
|
36
|
+
if (userId) {
|
|
37
|
+
await Promise.all([
|
|
38
|
+
queryClient.invalidateQueries({
|
|
39
|
+
queryKey: subscriptionStatusQueryKeys.user(userId),
|
|
40
|
+
}),
|
|
41
|
+
queryClient.invalidateQueries({
|
|
42
|
+
queryKey: creditsQueryKeys.user(userId),
|
|
43
|
+
}),
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
7
|
+
import { normalizeError } from "../../utils/errorUtils";
|
|
7
8
|
|
|
8
9
|
export interface ServiceCallState<T> {
|
|
9
10
|
data: T | null;
|
|
@@ -54,7 +55,7 @@ export function useServiceCall<T>(
|
|
|
54
55
|
setState({ data, isLoading: false, error: null });
|
|
55
56
|
onSuccessRef.current?.(data);
|
|
56
57
|
} catch (error) {
|
|
57
|
-
const errorObj = error
|
|
58
|
+
const errorObj = normalizeError(error, "Service call failed");
|
|
58
59
|
setState({ data: null, isLoading: false, error: errorObj });
|
|
59
60
|
onErrorRef.current?.(errorObj);
|
|
60
61
|
} finally {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Utilities
|
|
3
|
+
* Common error handling and normalization functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes unknown error types to Error objects
|
|
8
|
+
* Useful for catch blocks where error type is unknown
|
|
9
|
+
*
|
|
10
|
+
* @param error - The error to normalize (unknown type)
|
|
11
|
+
* @param fallbackMessage - Message to use if error is not an Error object
|
|
12
|
+
* @returns Always returns an Error object
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* try {
|
|
16
|
+
* await someOperation();
|
|
17
|
+
* } catch (error) {
|
|
18
|
+
* const err = normalizeError(error, "Operation failed");
|
|
19
|
+
* console.error(err.message);
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeError(
|
|
23
|
+
error: unknown,
|
|
24
|
+
fallbackMessage = "Unknown error"
|
|
25
|
+
): Error {
|
|
26
|
+
if (error instanceof Error) {
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const message = typeof error === "string" ? error : String(error);
|
|
31
|
+
return new Error(message || fallbackMessage);
|
|
32
|
+
}
|
package/src/utils/appUtils.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
import { Platform } from "react-native";
|
|
5
5
|
import Constants from "expo-constants";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Development mode flag
|
|
9
|
+
* Safe check for __DEV__ that works in all environments
|
|
10
|
+
*/
|
|
11
|
+
export const IS_DEV_MODE = typeof __DEV__ !== "undefined" && __DEV__;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Gets the current app version from Expo constants
|
|
9
15
|
*/
|
package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.constants.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const DAYS_REMAINING_WARNING_THRESHOLD = 7;
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.constants.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const EXPIRING_SOON_THRESHOLD_DAYS = 7;
|