@umituz/react-native-subscription 2.35.16 → 2.35.18
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 +1 -1
- package/src/domains/credits/presentation/useCredits.ts +6 -20
- 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/initializer/BackgroundInitializer.ts +2 -8
- 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/usePurchasePackage.ts +0 -44
- 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 +0 -39
- 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/wallet/presentation/hooks/useProductMetadata.ts +3 -6
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +3 -6
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.35.
|
|
3
|
+
"version": "2.35.18",
|
|
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,7 +1,9 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
|
-
import { useCallback, useMemo, useEffect
|
|
2
|
+
import { useCallback, useMemo, useEffect } from "react";
|
|
3
3
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
|
+
import { NO_CACHE_QUERY_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
|
|
6
|
+
import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
|
|
5
7
|
import {
|
|
6
8
|
getCreditsRepository,
|
|
7
9
|
getCreditsConfig,
|
|
@@ -44,29 +46,13 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
44
46
|
return result.data ?? null;
|
|
45
47
|
},
|
|
46
48
|
enabled: queryEnabled,
|
|
47
|
-
|
|
48
|
-
staleTime: 0,
|
|
49
|
-
refetchOnMount: "always",
|
|
50
|
-
refetchOnWindowFocus: "always",
|
|
51
|
-
refetchOnReconnect: "always",
|
|
49
|
+
...NO_CACHE_QUERY_CONFIG,
|
|
52
50
|
});
|
|
53
51
|
|
|
54
52
|
const queryClient = useQueryClient();
|
|
55
53
|
|
|
56
|
-
//
|
|
57
|
-
|
|
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]);
|
|
54
|
+
// Clean up previous user's cache on logout/user switch
|
|
55
|
+
usePreviousUserCleanup(userId, queryClient, creditsQueryKeys.user);
|
|
70
56
|
|
|
71
57
|
useEffect(() => {
|
|
72
58
|
if (!isAuthenticated(userId)) return undefined;
|
|
@@ -7,8 +7,6 @@ import type { PurchasesPackage } from "react-native-purchases";
|
|
|
7
7
|
import { usePurchaseLoadingStore } from "../../subscription/presentation/stores";
|
|
8
8
|
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
9
9
|
|
|
10
|
-
declare const __DEV__: boolean;
|
|
11
|
-
|
|
12
10
|
export interface UsePaywallActionsParams {
|
|
13
11
|
packages?: PurchasesPackage[];
|
|
14
12
|
onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
|
|
@@ -54,35 +52,18 @@ export function usePaywallActions({
|
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
const handlePurchase = useCallback(async () => {
|
|
57
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
58
|
-
console.log("[usePaywallActions] handlePurchase called", {
|
|
59
|
-
selectedPlanId,
|
|
60
|
-
hasOnPurchase: !!onPurchaseRef.current,
|
|
61
|
-
isProcessing,
|
|
62
|
-
packagesCount: packages.length,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
55
|
|
|
66
56
|
if (!selectedPlanId) {
|
|
67
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
|
-
console.warn("[usePaywallActions] ❌ No plan selected");
|
|
69
|
-
}
|
|
70
57
|
return;
|
|
71
58
|
}
|
|
72
59
|
|
|
73
60
|
if (!onPurchaseRef.current) {
|
|
74
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
-
console.error("[usePaywallActions] ❌ No onPurchase callback provided");
|
|
76
|
-
}
|
|
77
61
|
const err = new Error("Purchase handler not configured");
|
|
78
62
|
onPurchaseErrorRef.current?.(err);
|
|
79
63
|
return;
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
if (isProcessing) {
|
|
83
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
-
console.warn("[usePaywallActions] ⚠️ Already processing, ignoring duplicate request");
|
|
85
|
-
}
|
|
86
67
|
return;
|
|
87
68
|
}
|
|
88
69
|
|
|
@@ -100,117 +81,56 @@ export function usePaywallActions({
|
|
|
100
81
|
return;
|
|
101
82
|
}
|
|
102
83
|
|
|
103
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
104
|
-
console.log("[usePaywallActions] ✅ Starting purchase", {
|
|
105
|
-
productId: pkg.product.identifier,
|
|
106
|
-
title: pkg.product.title,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
84
|
setIsLocalProcessing(true);
|
|
111
85
|
startPurchase(selectedPlanId, "manual");
|
|
112
86
|
|
|
113
87
|
try {
|
|
114
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
115
|
-
console.log("[usePaywallActions] 🚀 Calling onPurchase callback");
|
|
116
|
-
}
|
|
117
88
|
|
|
118
89
|
const success = await onPurchaseRef.current(pkg);
|
|
119
90
|
|
|
120
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
121
|
-
console.log("[usePaywallActions] 📦 Purchase result:", { success, type: typeof success });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
91
|
if (success === true) {
|
|
125
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
-
console.log("[usePaywallActions] ✅ Purchase successful, calling success callbacks");
|
|
127
|
-
}
|
|
128
92
|
onPurchaseSuccessRef.current?.();
|
|
129
93
|
onCloseRef.current?.();
|
|
130
94
|
} else if (success === false) {
|
|
131
95
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
132
96
|
console.warn("[usePaywallActions] ⚠️ Purchase returned false (user cancelled or failed)");
|
|
133
97
|
}
|
|
134
|
-
} else {
|
|
135
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
136
|
-
console.error("[usePaywallActions] ❌ Purchase returned unexpected value:", success);
|
|
137
|
-
}
|
|
138
98
|
}
|
|
99
|
+
// else: success is undefined/null - no action needed
|
|
139
100
|
} catch (error) {
|
|
140
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
141
|
-
console.error("[usePaywallActions] ❌ Purchase error:", error);
|
|
142
|
-
}
|
|
143
101
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
144
102
|
onPurchaseErrorRef.current?.(err);
|
|
145
103
|
} finally {
|
|
146
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
147
|
-
console.log("[usePaywallActions] 🏁 Purchase completed, cleaning up");
|
|
148
|
-
}
|
|
149
104
|
setIsLocalProcessing(false);
|
|
150
105
|
endPurchase(selectedPlanId);
|
|
151
106
|
}
|
|
152
107
|
}, [selectedPlanId, packages, isProcessing, startPurchase, endPurchase]);
|
|
153
108
|
|
|
154
109
|
const handleRestore = useCallback(async () => {
|
|
155
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
156
|
-
console.log("[usePaywallActions] handleRestore called", {
|
|
157
|
-
hasOnRestore: !!onRestoreRef.current,
|
|
158
|
-
isProcessing,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
110
|
|
|
162
111
|
if (!onRestoreRef.current) {
|
|
163
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
164
|
-
console.error("[usePaywallActions] ❌ No onRestore callback provided");
|
|
165
|
-
}
|
|
166
112
|
const err = new Error("Restore handler not configured");
|
|
167
113
|
onPurchaseErrorRef.current?.(err);
|
|
168
114
|
return;
|
|
169
115
|
}
|
|
170
116
|
|
|
171
117
|
if (isProcessing) {
|
|
172
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
173
|
-
console.warn("[usePaywallActions] ⚠️ Already processing, ignoring restore request");
|
|
174
|
-
}
|
|
175
118
|
return;
|
|
176
119
|
}
|
|
177
120
|
|
|
178
121
|
setIsLocalProcessing(true);
|
|
179
122
|
try {
|
|
180
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
181
|
-
console.log("[usePaywallActions] 🚀 Calling onRestore callback");
|
|
182
|
-
}
|
|
183
123
|
|
|
184
124
|
const success = await onRestoreRef.current();
|
|
185
125
|
|
|
186
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
187
|
-
console.log("[usePaywallActions] 📦 Restore result:", { success, type: typeof success });
|
|
188
|
-
}
|
|
189
|
-
|
|
190
126
|
if (success === true) {
|
|
191
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
192
|
-
console.log("[usePaywallActions] ✅ Restore successful");
|
|
193
|
-
}
|
|
194
127
|
onPurchaseSuccessRef.current?.();
|
|
195
|
-
} else if (success === false) {
|
|
196
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
197
|
-
console.warn("[usePaywallActions] ⚠️ Restore returned false");
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
201
|
-
console.error("[usePaywallActions] ❌ Restore returned unexpected value:", success);
|
|
202
|
-
}
|
|
203
128
|
}
|
|
129
|
+
// else: success is false/undefined - restore failed or user cancelled, no action needed
|
|
204
130
|
} catch (error) {
|
|
205
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
206
|
-
console.error("[usePaywallActions] ❌ Restore error:", error);
|
|
207
|
-
}
|
|
208
131
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
209
132
|
onPurchaseErrorRef.current?.(err);
|
|
210
133
|
} finally {
|
|
211
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
212
|
-
console.log("[usePaywallActions] 🏁 Restore completed");
|
|
213
|
-
}
|
|
214
134
|
setIsLocalProcessing(false);
|
|
215
135
|
}
|
|
216
136
|
}, [isProcessing]);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Customer Info Helper Functions
|
|
3
|
+
* Utilities for extracting data from RevenueCat CustomerInfo objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CustomerInfo } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts active entitlement IDs from CustomerInfo
|
|
10
|
+
* Useful for logging and debugging
|
|
11
|
+
*
|
|
12
|
+
* @param customerInfo - RevenueCat CustomerInfo object
|
|
13
|
+
* @returns Array of active entitlement IDs
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const activeIds = getActiveEntitlementIds(customerInfo);
|
|
17
|
+
* console.log("Active entitlements:", activeIds); // ["premium", "pro_features"]
|
|
18
|
+
*/
|
|
19
|
+
export function getActiveEntitlementIds(customerInfo: CustomerInfo): string[] {
|
|
20
|
+
return Object.keys(customerInfo.entitlements.active);
|
|
21
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FirebaseAuthLike } from "./SubscriptionInitializerTypes";
|
|
2
2
|
|
|
3
|
-
declare const __DEV__: boolean;
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Gets the current user ID from Firebase auth.
|
|
7
5
|
* Returns undefined for anonymous users to let RevenueCat generate its own anonymous ID.
|
|
@@ -9,17 +7,11 @@ declare const __DEV__: boolean;
|
|
|
9
7
|
export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
|
|
10
8
|
const auth = getAuth();
|
|
11
9
|
if (!auth) {
|
|
12
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
13
|
-
console.log('[SubscriptionAuthListener] No auth available');
|
|
14
|
-
}
|
|
15
10
|
return undefined;
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
const user = auth.currentUser;
|
|
19
14
|
if (!user) {
|
|
20
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
21
|
-
console.log('[SubscriptionAuthListener] No current user');
|
|
22
|
-
}
|
|
23
15
|
return undefined;
|
|
24
16
|
}
|
|
25
17
|
|
|
@@ -30,10 +22,6 @@ export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string
|
|
|
30
22
|
return undefined;
|
|
31
23
|
}
|
|
32
24
|
|
|
33
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
34
|
-
console.log('[SubscriptionAuthListener] Authenticated user:', user.uid);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
25
|
return user.uid;
|
|
38
26
|
};
|
|
39
27
|
|
|
@@ -48,16 +36,9 @@ export const setupAuthStateListener = (
|
|
|
48
36
|
): (() => void) | null => {
|
|
49
37
|
const auth = getAuth();
|
|
50
38
|
if (!auth) {
|
|
51
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
52
|
-
console.log('[SubscriptionAuthListener] Cannot setup listener - no auth available');
|
|
53
|
-
}
|
|
54
39
|
return null;
|
|
55
40
|
}
|
|
56
41
|
|
|
57
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
58
|
-
console.log('[SubscriptionAuthListener] Setting up auth state listener');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
42
|
return auth.onAuthStateChanged((user) => {
|
|
62
43
|
const userId = (user && !user.isAnonymous) ? user.uid : undefined;
|
|
63
44
|
|
|
@@ -2,8 +2,6 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
|
|
|
2
2
|
import { getCurrentUserId, setupAuthStateListener } from "../SubscriptionAuthListener";
|
|
3
3
|
import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
|
|
4
4
|
|
|
5
|
-
declare const __DEV__: boolean;
|
|
6
|
-
|
|
7
5
|
export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
|
|
8
6
|
const initializeInBackground = async (revenueCatUserId?: string): Promise<void> => {
|
|
9
7
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
@@ -17,10 +15,6 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
17
15
|
throw new Error("Firebase auth is not available");
|
|
18
16
|
}
|
|
19
17
|
|
|
20
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
21
|
-
console.log('[BackgroundInitializer] Starting background initialization');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
const initialRevenueCatUserId = getCurrentUserId(() => auth);
|
|
25
19
|
|
|
26
20
|
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
@@ -35,8 +29,8 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
|
|
|
35
29
|
}
|
|
36
30
|
try {
|
|
37
31
|
await initializeInBackground(newRevenueCatUserId);
|
|
38
|
-
} catch (
|
|
39
|
-
|
|
32
|
+
} catch (_error) {
|
|
33
|
+
// Background re-initialization errors are non-critical, already logged by SubscriptionManager
|
|
40
34
|
}
|
|
41
35
|
});
|
|
42
36
|
|
|
@@ -6,8 +6,6 @@ import { emitCreditsUpdated } from "./syncEventEmitter";
|
|
|
6
6
|
import { generateInitSyncId, generateStatusSyncId } from "./syncIdGenerators";
|
|
7
7
|
import { NO_SUBSCRIPTION_PRODUCT_ID, DEFAULT_FREE_USER_DATA } from "./syncConstants";
|
|
8
8
|
|
|
9
|
-
declare const __DEV__: boolean;
|
|
10
|
-
|
|
11
9
|
export const handleExpiredSubscription = async (userId: string): Promise<void> => {
|
|
12
10
|
await getCreditsRepository().syncExpiredStatus(userId);
|
|
13
11
|
emitCreditsUpdated(userId);
|
|
@@ -34,16 +32,6 @@ export const handlePremiumStatusSync = async (
|
|
|
34
32
|
willRenew: boolean,
|
|
35
33
|
periodType: PeriodType | null
|
|
36
34
|
): Promise<void> => {
|
|
37
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
38
|
-
console.log("[StatusChangeHandlers] handlePremiumStatusSync called:", {
|
|
39
|
-
userId,
|
|
40
|
-
isPremium,
|
|
41
|
-
productId,
|
|
42
|
-
expiresAt,
|
|
43
|
-
willRenew,
|
|
44
|
-
periodType,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
35
|
|
|
48
36
|
const revenueCatData: RevenueCatData = {
|
|
49
37
|
expirationDate: expiresAt,
|
|
@@ -60,16 +48,6 @@ export const handlePremiumStatusSync = async (
|
|
|
60
48
|
|
|
61
49
|
const statusSyncId = generateStatusSyncId(userId, isPremium);
|
|
62
50
|
|
|
63
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
|
-
console.log("[StatusChangeHandlers] Calling initializeCredits with:", {
|
|
65
|
-
userId,
|
|
66
|
-
statusSyncId,
|
|
67
|
-
productId,
|
|
68
|
-
source: PURCHASE_SOURCE.SETTINGS,
|
|
69
|
-
type: PURCHASE_TYPE.INITIAL,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
51
|
await getCreditsRepository().initializeCredits(
|
|
74
52
|
userId,
|
|
75
53
|
statusSyncId,
|
|
@@ -79,13 +57,5 @@ export const handlePremiumStatusSync = async (
|
|
|
79
57
|
PURCHASE_TYPE.INITIAL
|
|
80
58
|
);
|
|
81
59
|
|
|
82
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
83
|
-
console.log("[StatusChangeHandlers] initializeCredits completed, emitting credits updated event");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
60
|
emitCreditsUpdated(userId);
|
|
87
|
-
|
|
88
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
89
|
-
console.log("[StatusChangeHandlers] ✅ handlePremiumStatusSync completed successfully");
|
|
90
|
-
}
|
|
91
61
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Threshold Constants
|
|
3
|
+
* Centralized threshold values for subscription UI logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Number of days before expiration to show warnings
|
|
8
|
+
* Used across subscription UI components for consistency
|
|
9
|
+
*/
|
|
10
|
+
export const EXPIRATION_WARNING_THRESHOLD_DAYS = 7;
|
|
@@ -32,15 +32,15 @@ export class PurchaseStatusResolver {
|
|
|
32
32
|
expirationDate: toDate(entitlement.expirationDate),
|
|
33
33
|
willRenew: entitlement.willRenew ?? false,
|
|
34
34
|
productIdentifier,
|
|
35
|
-
originalPurchaseDate: toDate(entitlement.originalPurchaseDate)
|
|
36
|
-
latestPurchaseDate: toDate(entitlement.latestPurchaseDate)
|
|
35
|
+
originalPurchaseDate: toDate(entitlement.originalPurchaseDate),
|
|
36
|
+
latestPurchaseDate: toDate(entitlement.latestPurchaseDate),
|
|
37
37
|
billingIssuesDetected: entitlement.billingIssueDetectedAt !== null && entitlement.billingIssueDetectedAt !== undefined,
|
|
38
38
|
isSandbox: entitlement.isSandbox ?? false,
|
|
39
39
|
periodType: entitlement.periodType ?? null,
|
|
40
40
|
packageType: detectedPackageType,
|
|
41
41
|
store: null,
|
|
42
42
|
gracePeriodExpiresDate: null,
|
|
43
|
-
unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt)
|
|
43
|
+
unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt),
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
2
2
|
import type { IRevenueCatService } from "../../../../../shared/application/ports/IRevenueCatService";
|
|
3
3
|
|
|
4
|
-
declare const __DEV__: boolean;
|
|
5
|
-
|
|
6
4
|
export async function fetchPackages(
|
|
7
5
|
service: IRevenueCatService
|
|
8
6
|
): Promise<PurchasesPackage[]> {
|
|
@@ -13,26 +11,12 @@ export async function fetchPackages(
|
|
|
13
11
|
try {
|
|
14
12
|
const offering = await service.fetchOfferings();
|
|
15
13
|
|
|
16
|
-
if (__DEV__) {
|
|
17
|
-
console.log('[PackageHandler] fetchOfferings result:', {
|
|
18
|
-
hasOffering: !!offering,
|
|
19
|
-
offeringId: offering?.identifier,
|
|
20
|
-
packagesCount: offering?.availablePackages?.length,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
if (!offering) {
|
|
25
|
-
if (__DEV__) {
|
|
26
|
-
console.warn('[PackageHandler] No offering returned, returning empty array');
|
|
27
|
-
}
|
|
28
15
|
return [];
|
|
29
16
|
}
|
|
30
17
|
|
|
31
18
|
const packages = offering.availablePackages;
|
|
32
19
|
if (!packages || packages.length === 0) {
|
|
33
|
-
if (__DEV__) {
|
|
34
|
-
console.warn('[PackageHandler] Offering has no packages, returning empty array');
|
|
35
|
-
}
|
|
36
20
|
return [];
|
|
37
21
|
}
|
|
38
22
|
|
|
@@ -45,9 +29,6 @@ export async function fetchPackages(
|
|
|
45
29
|
|
|
46
30
|
return packages;
|
|
47
31
|
} catch (error) {
|
|
48
|
-
if (__DEV__) {
|
|
49
|
-
console.error('[PackageHandler] Error fetching packages:', error);
|
|
50
|
-
}
|
|
51
32
|
throw new Error(
|
|
52
33
|
`Failed to fetch subscription packages. ${
|
|
53
34
|
error instanceof Error ? error.message : "Unknown error"
|
|
@@ -45,7 +45,7 @@ export function useCustomerInfo(): UseCustomerInfoResult {
|
|
|
45
45
|
listenerRef.current = null;
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
-
}, []); // fetchCustomerInfo is stable,
|
|
48
|
+
}, [fetchCustomerInfo]); // fetchCustomerInfo is stable (empty deps), included for lint
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
51
|
customerInfo,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
|
|
7
7
|
import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
|
|
8
8
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
9
|
+
import { requireAuthentication } from "../../utils/authGuards";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Initialize subscription with RevenueCat
|
|
@@ -15,10 +16,7 @@ export const useInitializeSubscription = (userId: string | undefined) => {
|
|
|
15
16
|
|
|
16
17
|
return useMutation({
|
|
17
18
|
mutationFn: async () => {
|
|
18
|
-
|
|
19
|
-
throw new Error("User not authenticated");
|
|
20
|
-
}
|
|
21
|
-
|
|
19
|
+
requireAuthentication(userId);
|
|
22
20
|
return SubscriptionManager.initialize(userId);
|
|
23
21
|
},
|
|
24
22
|
onSuccess: () => {
|
|
@@ -19,15 +19,12 @@ import { subscriptionStatusQueryKeys } from "../../presentation/useSubscriptionS
|
|
|
19
19
|
import { creditsQueryKeys } from "../../../credits/presentation/creditsQueryKeys";
|
|
20
20
|
import { getErrorMessage } from "../../../revenuecat/core/errors";
|
|
21
21
|
|
|
22
|
-
declare const __DEV__: boolean;
|
|
23
|
-
|
|
24
22
|
/** Purchase mutation result - simplified for presentation layer */
|
|
25
23
|
export interface PurchaseMutationResult {
|
|
26
24
|
success: boolean;
|
|
27
25
|
productId: string;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
|
|
31
28
|
/**
|
|
32
29
|
* Purchase a subscription package
|
|
33
30
|
* Credits are initialized by CustomerInfoListener when entitlement becomes active
|
|
@@ -41,26 +38,11 @@ export const usePurchasePackage = () => {
|
|
|
41
38
|
|
|
42
39
|
return useMutation({
|
|
43
40
|
mutationFn: async (pkg: PurchasesPackage): Promise<PurchaseMutationResult> => {
|
|
44
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
45
|
-
console.log("[Purchase] ========================================");
|
|
46
|
-
console.log("[Purchase] mutationFn called:", {
|
|
47
|
-
productId: pkg.product.identifier,
|
|
48
|
-
userId,
|
|
49
|
-
isAnonymous,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
41
|
if (!userId) {
|
|
54
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
55
|
-
console.log("[Purchase] ERROR: User not authenticated");
|
|
56
|
-
}
|
|
57
42
|
throw new Error("User not authenticated");
|
|
58
43
|
}
|
|
59
44
|
|
|
60
45
|
if (isAnonymous) {
|
|
61
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
62
|
-
console.log("[Purchase] ERROR: Anonymous users cannot purchase");
|
|
63
|
-
}
|
|
64
46
|
throw new Error("Anonymous users cannot purchase subscriptions");
|
|
65
47
|
}
|
|
66
48
|
|
|
@@ -71,21 +53,11 @@ export const usePurchasePackage = () => {
|
|
|
71
53
|
|
|
72
54
|
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
73
55
|
|
|
74
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
-
console.log("[Purchase] Purchase completed:", { success, productId });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
56
|
return { success, productId };
|
|
79
57
|
},
|
|
80
58
|
onSuccess: (result) => {
|
|
81
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
82
|
-
console.log("[Purchase] onSuccess called:", result);
|
|
83
|
-
}
|
|
84
59
|
|
|
85
60
|
if (result.success) {
|
|
86
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
87
|
-
console.log("[Purchase] ✅ Purchase successful! Invalidating queries...");
|
|
88
|
-
}
|
|
89
61
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
90
62
|
queryClient.invalidateQueries({
|
|
91
63
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
@@ -97,34 +69,18 @@ export const usePurchasePackage = () => {
|
|
|
97
69
|
queryClient.invalidateQueries({
|
|
98
70
|
queryKey: creditsQueryKeys.user(userId),
|
|
99
71
|
});
|
|
100
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
101
|
-
console.log("[Purchase] Queries invalidated - credits should reload now");
|
|
102
|
-
}
|
|
103
72
|
}
|
|
104
73
|
} else {
|
|
105
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
106
|
-
console.log("[Purchase] ❌ Purchase failed");
|
|
107
|
-
}
|
|
108
74
|
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
109
75
|
}
|
|
110
76
|
},
|
|
111
77
|
onError: (error) => {
|
|
112
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
113
|
-
console.log("[Purchase] onError called:", error);
|
|
114
|
-
}
|
|
115
78
|
|
|
116
79
|
// Use map-based lookup - O(1) complexity
|
|
117
80
|
const errorInfo = getErrorMessage(error);
|
|
118
81
|
|
|
119
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
120
|
-
console.log("[Purchase] Error info:", errorInfo);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
82
|
// Don't show alert for user cancellation
|
|
124
83
|
if (!errorInfo.shouldShowAlert) {
|
|
125
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
126
|
-
console.log("[Purchase] User cancelled - not showing alert");
|
|
127
|
-
}
|
|
128
84
|
return;
|
|
129
85
|
}
|
|
130
86
|
|
|
@@ -3,22 +3,12 @@ import type { RevenueCatConfig } from "../../../revenuecat/core/types";
|
|
|
3
3
|
import { ListenerState } from "./listeners/ListenerState";
|
|
4
4
|
import { processCustomerInfo } from "./listeners/CustomerInfoHandler";
|
|
5
5
|
|
|
6
|
-
declare const __DEV__: boolean;
|
|
7
|
-
|
|
8
6
|
export class CustomerInfoListenerManager {
|
|
9
7
|
private state = new ListenerState();
|
|
10
8
|
|
|
11
9
|
setUserId(userId: string, config: RevenueCatConfig): void {
|
|
12
10
|
const wasUserChange = this.state.hasUserChanged(userId);
|
|
13
11
|
|
|
14
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
15
|
-
console.log("[CustomerInfoListener] setUserId called:", {
|
|
16
|
-
userId,
|
|
17
|
-
wasUserChange,
|
|
18
|
-
hasListener: !!this.state.listener,
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
if (wasUserChange) {
|
|
23
13
|
this.removeListener();
|
|
24
14
|
this.state.resetRenewalState();
|
|
@@ -32,9 +22,6 @@ export class CustomerInfoListenerManager {
|
|
|
32
22
|
}
|
|
33
23
|
|
|
34
24
|
clearUserId(): void {
|
|
35
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
36
|
-
console.log("[CustomerInfoListener] clearUserId called");
|
|
37
|
-
}
|
|
38
25
|
this.state.currentUserId = null;
|
|
39
26
|
this.state.resetRenewalState();
|
|
40
27
|
}
|
|
@@ -42,10 +29,6 @@ export class CustomerInfoListenerManager {
|
|
|
42
29
|
setupListener(config: RevenueCatConfig): void {
|
|
43
30
|
this.removeListener();
|
|
44
31
|
|
|
45
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
46
|
-
console.log("[CustomerInfoListener] setupListener: Registering listener");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
this.state.listener = async (customerInfo: CustomerInfo) => {
|
|
50
33
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
51
34
|
console.log("[CustomerInfoListener] 🔔 LISTENER TRIGGERED!", {
|
|
@@ -57,9 +40,6 @@ export class CustomerInfoListenerManager {
|
|
|
57
40
|
|
|
58
41
|
const capturedUserId = this.state.currentUserId;
|
|
59
42
|
if (!capturedUserId) {
|
|
60
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
61
|
-
console.log("[CustomerInfoListener] No userId - skipping");
|
|
62
|
-
}
|
|
63
43
|
return;
|
|
64
44
|
}
|
|
65
45
|
|
|
@@ -72,21 +52,11 @@ export class CustomerInfoListenerManager {
|
|
|
72
52
|
|
|
73
53
|
if (this.state.currentUserId === capturedUserId) {
|
|
74
54
|
this.state.renewalState = newRenewalState;
|
|
75
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
76
|
-
console.log("[CustomerInfoListener] processCustomerInfo completed");
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
|
-
console.log("[CustomerInfoListener] User changed during processing - discarding result");
|
|
81
|
-
}
|
|
82
55
|
}
|
|
56
|
+
// else: User switched during async operation, discard stale renewal state
|
|
83
57
|
};
|
|
84
58
|
|
|
85
59
|
Purchases.addCustomerInfoUpdateListener(this.state.listener);
|
|
86
|
-
|
|
87
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
88
|
-
console.log("[CustomerInfoListener] Listener registered successfully");
|
|
89
|
-
}
|
|
90
60
|
}
|
|
91
61
|
|
|
92
62
|
removeListener(): void {
|