@umituz/react-native-subscription 2.6.1 → 2.8.0
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/index.ts +8 -0
- package/src/revenuecat/infrastructure/services/CustomerInfoListenerManager.ts +12 -0
- package/src/revenuecat/infrastructure/services/OfferingsFetcher.ts +27 -4
- package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +72 -4
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +67 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionQueries.ts +199 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
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",
|
package/src/index.ts
CHANGED
|
@@ -317,3 +317,11 @@ export {
|
|
|
317
317
|
|
|
318
318
|
export { useRevenueCat } from "./revenuecat/presentation/hooks/useRevenueCat";
|
|
319
319
|
export type { UseRevenueCatResult } from "./revenuecat/presentation/hooks/useRevenueCat";
|
|
320
|
+
|
|
321
|
+
export {
|
|
322
|
+
useInitializeSubscription,
|
|
323
|
+
useSubscriptionPackages,
|
|
324
|
+
usePurchasePackage,
|
|
325
|
+
useRestorePurchase,
|
|
326
|
+
SUBSCRIPTION_QUERY_KEYS,
|
|
327
|
+
} from "./revenuecat/presentation/hooks/useSubscriptionQueries";
|
|
@@ -9,6 +9,7 @@ import Purchases, {
|
|
|
9
9
|
} from "react-native-purchases";
|
|
10
10
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
11
11
|
import { syncPremiumStatus } from "../utils/PremiumStatusSyncer";
|
|
12
|
+
import { addPackageBreadcrumb } from "@umituz/react-native-sentry";
|
|
12
13
|
|
|
13
14
|
export class CustomerInfoListenerManager {
|
|
14
15
|
private listener: CustomerInfoUpdateListener | null = null;
|
|
@@ -30,12 +31,22 @@ export class CustomerInfoListenerManager {
|
|
|
30
31
|
setupListener(config: RevenueCatConfig): void {
|
|
31
32
|
this.removeListener();
|
|
32
33
|
|
|
34
|
+
addPackageBreadcrumb("subscription", "Setting up customer info listener", {
|
|
35
|
+
userId: this.currentUserId,
|
|
36
|
+
});
|
|
37
|
+
|
|
33
38
|
this.listener = (customerInfo: CustomerInfo) => {
|
|
34
39
|
if (!this.currentUserId) return;
|
|
35
40
|
|
|
36
41
|
const hasPremium =
|
|
37
42
|
!!customerInfo.entitlements.active[this.entitlementIdentifier];
|
|
38
43
|
|
|
44
|
+
addPackageBreadcrumb("subscription", "Customer info updated", {
|
|
45
|
+
userId: this.currentUserId,
|
|
46
|
+
hasPremium,
|
|
47
|
+
entitlementIdentifier: this.entitlementIdentifier,
|
|
48
|
+
});
|
|
49
|
+
|
|
39
50
|
if (__DEV__) {
|
|
40
51
|
console.log("[RevenueCat] CustomerInfo updated", {
|
|
41
52
|
userId: this.currentUserId,
|
|
@@ -52,6 +63,7 @@ export class CustomerInfoListenerManager {
|
|
|
52
63
|
|
|
53
64
|
removeListener(): void {
|
|
54
65
|
if (this.listener) {
|
|
66
|
+
addPackageBreadcrumb("subscription", "Removing customer info listener", {});
|
|
55
67
|
Purchases.removeCustomerInfoUpdateListener(this.listener);
|
|
56
68
|
this.listener = null;
|
|
57
69
|
}
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import Purchases, { type PurchasesOffering } from "react-native-purchases";
|
|
7
|
-
|
|
7
|
+
import {
|
|
8
|
+
trackPackageError,
|
|
9
|
+
addPackageBreadcrumb,
|
|
10
|
+
trackPackageWarning,
|
|
11
|
+
} from "@umituz/react-native-sentry";
|
|
8
12
|
|
|
9
13
|
export interface OfferingsFetcherDeps {
|
|
10
14
|
isInitialized: () => boolean;
|
|
@@ -14,6 +18,10 @@ export interface OfferingsFetcherDeps {
|
|
|
14
18
|
export async function fetchOfferings(
|
|
15
19
|
deps: OfferingsFetcherDeps
|
|
16
20
|
): Promise<PurchasesOffering | null> {
|
|
21
|
+
addPackageBreadcrumb("subscription", "Fetch offerings started", {
|
|
22
|
+
isInitialized: deps.isInitialized(),
|
|
23
|
+
});
|
|
24
|
+
|
|
17
25
|
if (__DEV__) {
|
|
18
26
|
console.log(
|
|
19
27
|
"[RevenueCat] fetchOfferings() called, isInitialized:",
|
|
@@ -22,26 +30,41 @@ export async function fetchOfferings(
|
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
if (!deps.isInitialized()) {
|
|
33
|
+
trackPackageWarning("subscription", "Fetch offerings called before initialization", {});
|
|
34
|
+
|
|
25
35
|
if (__DEV__) {
|
|
26
36
|
console.log("[RevenueCat] fetchOfferings() - NOT initialized");
|
|
27
37
|
}
|
|
28
38
|
return null;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
41
|
try {
|
|
34
42
|
const offerings = await Purchases.getOfferings();
|
|
35
43
|
|
|
44
|
+
const packagesCount = offerings.current?.availablePackages?.length ?? 0;
|
|
45
|
+
|
|
46
|
+
addPackageBreadcrumb("subscription", "Fetch offerings success", {
|
|
47
|
+
hasCurrent: !!offerings.current,
|
|
48
|
+
packagesCount,
|
|
49
|
+
});
|
|
50
|
+
|
|
36
51
|
if (__DEV__) {
|
|
37
52
|
console.log("[RevenueCat] fetchOfferings() result:", {
|
|
38
53
|
hasCurrent: !!offerings.current,
|
|
39
|
-
packagesCount
|
|
54
|
+
packagesCount,
|
|
40
55
|
});
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
return offerings.current;
|
|
44
59
|
} catch (error) {
|
|
60
|
+
trackPackageError(
|
|
61
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
62
|
+
{
|
|
63
|
+
packageName: "subscription",
|
|
64
|
+
operation: "fetch_offerings",
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
45
68
|
if (__DEV__) {
|
|
46
69
|
console.log("[RevenueCat] fetchOfferings() error:", error);
|
|
47
70
|
}
|
|
@@ -7,8 +7,12 @@ import Purchases from "react-native-purchases";
|
|
|
7
7
|
import type { InitializeResult } from "../../application/ports/IRevenueCatService";
|
|
8
8
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
9
9
|
import { getErrorMessage } from "../../domain/types/RevenueCatTypes";
|
|
10
|
-
|
|
11
10
|
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
11
|
+
import {
|
|
12
|
+
trackPackageError,
|
|
13
|
+
addPackageBreadcrumb,
|
|
14
|
+
trackPackageWarning,
|
|
15
|
+
} from "@umituz/react-native-sentry";
|
|
12
16
|
|
|
13
17
|
export interface InitializerDeps {
|
|
14
18
|
config: RevenueCatConfig;
|
|
@@ -24,6 +28,11 @@ export async function initializeSDK(
|
|
|
24
28
|
userId: string,
|
|
25
29
|
apiKey?: string
|
|
26
30
|
): Promise<InitializeResult> {
|
|
31
|
+
addPackageBreadcrumb("subscription", "SDK initialization started", {
|
|
32
|
+
userId,
|
|
33
|
+
hasApiKey: !!apiKey,
|
|
34
|
+
});
|
|
35
|
+
|
|
27
36
|
if (__DEV__) {
|
|
28
37
|
console.log("[RevenueCat] initializeSDK() called with userId:", userId);
|
|
29
38
|
}
|
|
@@ -32,6 +41,10 @@ export async function initializeSDK(
|
|
|
32
41
|
if (deps.isInitialized()) {
|
|
33
42
|
const currentUserId = deps.getCurrentUserId();
|
|
34
43
|
if (currentUserId === userId) {
|
|
44
|
+
addPackageBreadcrumb("subscription", "Already initialized, fetching current state", {
|
|
45
|
+
userId,
|
|
46
|
+
});
|
|
47
|
+
|
|
35
48
|
if (__DEV__) {
|
|
36
49
|
console.log("[RevenueCat] Already initialized with same userId, skipping configure");
|
|
37
50
|
}
|
|
@@ -45,20 +58,38 @@ export async function initializeSDK(
|
|
|
45
58
|
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
46
59
|
return { success: true, offering: offerings.current, hasPremium };
|
|
47
60
|
} catch (error) {
|
|
61
|
+
trackPackageError(
|
|
62
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
63
|
+
{
|
|
64
|
+
packageName: "subscription",
|
|
65
|
+
operation: "get_current_state",
|
|
66
|
+
userId,
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
48
70
|
if (__DEV__) {
|
|
49
71
|
console.log("[RevenueCat] Failed to get current state:", error);
|
|
50
72
|
}
|
|
51
73
|
return { success: false, offering: null, hasPremium: false };
|
|
52
74
|
}
|
|
53
75
|
} else {
|
|
76
|
+
addPackageBreadcrumb("subscription", "User changed, logging out previous user", {
|
|
77
|
+
previousUserId: currentUserId,
|
|
78
|
+
newUserId: userId,
|
|
79
|
+
});
|
|
80
|
+
|
|
54
81
|
if (__DEV__) {
|
|
55
82
|
console.log("[RevenueCat] Different userId, will re-configure");
|
|
56
83
|
}
|
|
57
84
|
// Different userId - need to logout first
|
|
58
85
|
try {
|
|
59
86
|
await Purchases.logOut();
|
|
60
|
-
} catch {
|
|
61
|
-
|
|
87
|
+
} catch (error) {
|
|
88
|
+
trackPackageWarning("subscription", "Logout failed during user change", {
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
previousUserId: currentUserId,
|
|
91
|
+
newUserId: userId,
|
|
92
|
+
});
|
|
62
93
|
}
|
|
63
94
|
}
|
|
64
95
|
}
|
|
@@ -67,6 +98,13 @@ export async function initializeSDK(
|
|
|
67
98
|
|
|
68
99
|
const key = apiKey || resolveApiKey(deps.config);
|
|
69
100
|
if (!key) {
|
|
101
|
+
const error = new Error("No RevenueCat API key available");
|
|
102
|
+
trackPackageError(error, {
|
|
103
|
+
packageName: "subscription",
|
|
104
|
+
operation: "sdk_init_no_key",
|
|
105
|
+
userId,
|
|
106
|
+
});
|
|
107
|
+
|
|
70
108
|
if (__DEV__) {
|
|
71
109
|
console.log("[RevenueCat] No API key available");
|
|
72
110
|
}
|
|
@@ -75,11 +113,17 @@ export async function initializeSDK(
|
|
|
75
113
|
|
|
76
114
|
try {
|
|
77
115
|
if (deps.isUsingTestStore()) {
|
|
116
|
+
addPackageBreadcrumb("subscription", "Using test store configuration", {
|
|
117
|
+
userId,
|
|
118
|
+
});
|
|
119
|
+
|
|
78
120
|
if (__DEV__) {
|
|
79
121
|
console.log("[RevenueCat] Using Test Store key");
|
|
80
122
|
}
|
|
81
123
|
}
|
|
82
124
|
|
|
125
|
+
addPackageBreadcrumb("subscription", "Configuring SDK", { userId });
|
|
126
|
+
|
|
83
127
|
if (__DEV__) {
|
|
84
128
|
console.log("[RevenueCat] Calling Purchases.configure()...");
|
|
85
129
|
}
|
|
@@ -88,6 +132,10 @@ export async function initializeSDK(
|
|
|
88
132
|
deps.setInitialized(true);
|
|
89
133
|
deps.setCurrentUserId(userId);
|
|
90
134
|
|
|
135
|
+
addPackageBreadcrumb("subscription", "SDK configured successfully", {
|
|
136
|
+
userId,
|
|
137
|
+
});
|
|
138
|
+
|
|
91
139
|
if (__DEV__) {
|
|
92
140
|
console.log("[RevenueCat] SDK configured successfully");
|
|
93
141
|
}
|
|
@@ -97,10 +145,19 @@ export async function initializeSDK(
|
|
|
97
145
|
Purchases.getOfferings(),
|
|
98
146
|
]);
|
|
99
147
|
|
|
148
|
+
const packagesCount = offerings.current?.availablePackages?.length ?? 0;
|
|
149
|
+
|
|
150
|
+
addPackageBreadcrumb("subscription", "Offerings fetched", {
|
|
151
|
+
userId,
|
|
152
|
+
hasCurrent: !!offerings.current,
|
|
153
|
+
packagesCount,
|
|
154
|
+
allOfferingsCount: Object.keys(offerings.all).length,
|
|
155
|
+
});
|
|
156
|
+
|
|
100
157
|
if (__DEV__) {
|
|
101
158
|
console.log("[RevenueCat] Fetched offerings:", {
|
|
102
159
|
hasCurrent: !!offerings.current,
|
|
103
|
-
packagesCount
|
|
160
|
+
packagesCount,
|
|
104
161
|
allOfferingsCount: Object.keys(offerings.all).length,
|
|
105
162
|
});
|
|
106
163
|
}
|
|
@@ -111,6 +168,17 @@ export async function initializeSDK(
|
|
|
111
168
|
return { success: true, offering: offerings.current, hasPremium };
|
|
112
169
|
} catch (error) {
|
|
113
170
|
const errorMessage = getErrorMessage(error, "RevenueCat init failed");
|
|
171
|
+
|
|
172
|
+
trackPackageError(
|
|
173
|
+
error instanceof Error ? error : new Error(errorMessage),
|
|
174
|
+
{
|
|
175
|
+
packageName: "subscription",
|
|
176
|
+
operation: "sdk_init",
|
|
177
|
+
userId,
|
|
178
|
+
errorMessage,
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
114
182
|
if (__DEV__) {
|
|
115
183
|
console.log("[RevenueCat] Init failed:", errorMessage);
|
|
116
184
|
}
|
|
@@ -7,6 +7,10 @@ import type { CustomerInfo } from "react-native-purchases";
|
|
|
7
7
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
9
|
import { getExpirationDate } from "./ExpirationDateCalculator";
|
|
10
|
+
import {
|
|
11
|
+
trackPackageError,
|
|
12
|
+
addPackageBreadcrumb,
|
|
13
|
+
} from "@umituz/react-native-sentry";
|
|
10
14
|
|
|
11
15
|
export async function syncPremiumStatus(
|
|
12
16
|
config: RevenueCatConfig,
|
|
@@ -23,6 +27,14 @@ export async function syncPremiumStatus(
|
|
|
23
27
|
entitlementIdentifier
|
|
24
28
|
);
|
|
25
29
|
|
|
30
|
+
const isPremium = !!premiumEntitlement;
|
|
31
|
+
|
|
32
|
+
addPackageBreadcrumb("subscription", "Syncing premium status", {
|
|
33
|
+
userId,
|
|
34
|
+
isPremium,
|
|
35
|
+
productId: premiumEntitlement?.productIdentifier,
|
|
36
|
+
});
|
|
37
|
+
|
|
26
38
|
try {
|
|
27
39
|
if (premiumEntitlement) {
|
|
28
40
|
const productId = premiumEntitlement.productIdentifier;
|
|
@@ -36,7 +48,22 @@ export async function syncPremiumStatus(
|
|
|
36
48
|
} else {
|
|
37
49
|
await config.onPremiumStatusChanged(userId, false);
|
|
38
50
|
}
|
|
51
|
+
|
|
52
|
+
addPackageBreadcrumb("subscription", "Premium status synced successfully", {
|
|
53
|
+
userId,
|
|
54
|
+
isPremium,
|
|
55
|
+
});
|
|
39
56
|
} catch (error) {
|
|
57
|
+
trackPackageError(
|
|
58
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
59
|
+
{
|
|
60
|
+
packageName: "subscription",
|
|
61
|
+
operation: "sync_premium_status",
|
|
62
|
+
userId,
|
|
63
|
+
isPremium,
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
40
67
|
if (__DEV__) {
|
|
41
68
|
const message =
|
|
42
69
|
error instanceof Error ? error.message : "Premium sync failed";
|
|
@@ -55,9 +82,29 @@ export async function notifyPurchaseCompleted(
|
|
|
55
82
|
return;
|
|
56
83
|
}
|
|
57
84
|
|
|
85
|
+
addPackageBreadcrumb("subscription", "Notifying purchase completed", {
|
|
86
|
+
userId,
|
|
87
|
+
productId,
|
|
88
|
+
});
|
|
89
|
+
|
|
58
90
|
try {
|
|
59
91
|
await config.onPurchaseCompleted(userId, productId, customerInfo);
|
|
92
|
+
|
|
93
|
+
addPackageBreadcrumb("subscription", "Purchase callback completed", {
|
|
94
|
+
userId,
|
|
95
|
+
productId,
|
|
96
|
+
});
|
|
60
97
|
} catch (error) {
|
|
98
|
+
trackPackageError(
|
|
99
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
100
|
+
{
|
|
101
|
+
packageName: "subscription",
|
|
102
|
+
operation: "purchase_callback",
|
|
103
|
+
userId,
|
|
104
|
+
productId,
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
61
108
|
if (__DEV__) {
|
|
62
109
|
const message =
|
|
63
110
|
error instanceof Error ? error.message : "Purchase callback failed";
|
|
@@ -76,9 +123,29 @@ export async function notifyRestoreCompleted(
|
|
|
76
123
|
return;
|
|
77
124
|
}
|
|
78
125
|
|
|
126
|
+
addPackageBreadcrumb("subscription", "Notifying restore completed", {
|
|
127
|
+
userId,
|
|
128
|
+
isPremium,
|
|
129
|
+
});
|
|
130
|
+
|
|
79
131
|
try {
|
|
80
132
|
await config.onRestoreCompleted(userId, isPremium, customerInfo);
|
|
133
|
+
|
|
134
|
+
addPackageBreadcrumb("subscription", "Restore callback completed", {
|
|
135
|
+
userId,
|
|
136
|
+
isPremium,
|
|
137
|
+
});
|
|
81
138
|
} catch (error) {
|
|
139
|
+
trackPackageError(
|
|
140
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
141
|
+
{
|
|
142
|
+
packageName: "subscription",
|
|
143
|
+
operation: "restore_callback",
|
|
144
|
+
userId,
|
|
145
|
+
isPremium,
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
82
149
|
if (__DEV__) {
|
|
83
150
|
const message =
|
|
84
151
|
error instanceof Error ? error.message : "Restore callback failed";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription TanStack Query Hooks
|
|
3
|
+
* Server state management for RevenueCat subscriptions
|
|
4
|
+
* Generic hooks for 100+ apps
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
|
+
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
10
|
+
import {
|
|
11
|
+
trackPackageError,
|
|
12
|
+
addPackageBreadcrumb,
|
|
13
|
+
} from "@umituz/react-native-sentry";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Query keys for TanStack Query
|
|
17
|
+
*/
|
|
18
|
+
export const SUBSCRIPTION_QUERY_KEYS = {
|
|
19
|
+
packages: ["subscription", "packages"] as const,
|
|
20
|
+
initialized: (userId: string) =>
|
|
21
|
+
["subscription", "initialized", userId] as const,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
const STALE_TIME = 5 * 60 * 1000; // 5 minutes
|
|
25
|
+
const GC_TIME = 30 * 60 * 1000; // 30 minutes
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize subscription with RevenueCat
|
|
29
|
+
*/
|
|
30
|
+
export const useInitializeSubscription = (userId: string | undefined) => {
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
return useMutation({
|
|
34
|
+
mutationFn: async () => {
|
|
35
|
+
if (!userId) {
|
|
36
|
+
throw new Error("User not authenticated");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
addPackageBreadcrumb("subscription", "Initialize mutation started", {
|
|
40
|
+
userId,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return SubscriptionManager.initialize(userId);
|
|
44
|
+
},
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
if (userId) {
|
|
47
|
+
queryClient.invalidateQueries({
|
|
48
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
addPackageBreadcrumb(
|
|
52
|
+
"subscription",
|
|
53
|
+
"Initialize mutation success - packages invalidated",
|
|
54
|
+
{ userId }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
onError: (error) => {
|
|
59
|
+
trackPackageError(
|
|
60
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
61
|
+
{
|
|
62
|
+
packageName: "subscription",
|
|
63
|
+
operation: "initialize_mutation",
|
|
64
|
+
userId: userId ?? "NO_USER",
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fetch available subscription packages
|
|
73
|
+
*/
|
|
74
|
+
export const useSubscriptionPackages = (userId: string | undefined) => {
|
|
75
|
+
return useQuery({
|
|
76
|
+
queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId] as const,
|
|
77
|
+
queryFn: async () => {
|
|
78
|
+
addPackageBreadcrumb("subscription", "Fetch packages query started", {
|
|
79
|
+
userId: userId ?? "NO_USER",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Skip if already initialized for this specific user
|
|
83
|
+
if (!userId || !SubscriptionManager.isInitializedForUser(userId)) {
|
|
84
|
+
await SubscriptionManager.initialize(userId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const packages = await SubscriptionManager.getPackages();
|
|
88
|
+
|
|
89
|
+
addPackageBreadcrumb("subscription", "Fetch packages query success", {
|
|
90
|
+
userId: userId ?? "NO_USER",
|
|
91
|
+
count: packages.length,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return packages;
|
|
95
|
+
},
|
|
96
|
+
staleTime: STALE_TIME,
|
|
97
|
+
gcTime: GC_TIME,
|
|
98
|
+
enabled: !!userId, // Only run when userId is available
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Purchase a subscription package
|
|
104
|
+
*/
|
|
105
|
+
export const usePurchasePackage = (userId: string | undefined) => {
|
|
106
|
+
const queryClient = useQueryClient();
|
|
107
|
+
|
|
108
|
+
return useMutation({
|
|
109
|
+
mutationFn: async (pkg: PurchasesPackage) => {
|
|
110
|
+
if (!userId) {
|
|
111
|
+
throw new Error("User not authenticated");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
addPackageBreadcrumb("subscription", "Purchase mutation started", {
|
|
115
|
+
packageId: pkg.identifier,
|
|
116
|
+
userId,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
120
|
+
|
|
121
|
+
if (success) {
|
|
122
|
+
addPackageBreadcrumb("subscription", "Purchase mutation success", {
|
|
123
|
+
packageId: pkg.identifier,
|
|
124
|
+
userId,
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
addPackageBreadcrumb("subscription", "Purchase mutation failed", {
|
|
128
|
+
packageId: pkg.identifier,
|
|
129
|
+
userId,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return success;
|
|
134
|
+
},
|
|
135
|
+
onSuccess: () => {
|
|
136
|
+
queryClient.invalidateQueries({
|
|
137
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
onError: (error) => {
|
|
141
|
+
trackPackageError(
|
|
142
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
143
|
+
{
|
|
144
|
+
packageName: "subscription",
|
|
145
|
+
operation: "purchase_mutation",
|
|
146
|
+
userId: userId ?? "NO_USER",
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Restore previous purchases
|
|
155
|
+
*/
|
|
156
|
+
export const useRestorePurchase = (userId: string | undefined) => {
|
|
157
|
+
const queryClient = useQueryClient();
|
|
158
|
+
|
|
159
|
+
return useMutation({
|
|
160
|
+
mutationFn: async () => {
|
|
161
|
+
if (!userId) {
|
|
162
|
+
throw new Error("User not authenticated");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
addPackageBreadcrumb("subscription", "Restore mutation started", {
|
|
166
|
+
userId,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const success = await SubscriptionManager.restore();
|
|
170
|
+
|
|
171
|
+
if (success) {
|
|
172
|
+
addPackageBreadcrumb("subscription", "Restore mutation success", {
|
|
173
|
+
userId,
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
addPackageBreadcrumb("subscription", "Restore mutation failed", {
|
|
177
|
+
userId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return success;
|
|
182
|
+
},
|
|
183
|
+
onSuccess: () => {
|
|
184
|
+
queryClient.invalidateQueries({
|
|
185
|
+
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
onError: (error) => {
|
|
189
|
+
trackPackageError(
|
|
190
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
191
|
+
{
|
|
192
|
+
packageName: "subscription",
|
|
193
|
+
operation: "restore_mutation",
|
|
194
|
+
userId: userId ?? "NO_USER",
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
};
|