@umituz/react-native-subscription 2.17.6 → 2.17.7
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/paywall/hooks/usePaywallActions.ts +9 -12
- package/src/presentation/hooks/index.ts +0 -2
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +19 -0
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +18 -1
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +55 -0
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +24 -57
- package/src/presentation/hooks/useCompletePendingPurchase.ts +0 -104
- package/src/presentation/hooks/usePendingPurchase.ts +0 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.7",
|
|
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",
|
|
@@ -2,14 +2,12 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { usePurchasePackage } from "../../../revenuecat/presentation/hooks/usePurchasePackage";
|
|
4
4
|
import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRestorePurchase";
|
|
5
|
-
import { usePendingPurchase } from "../../../presentation/hooks/usePendingPurchase";
|
|
6
5
|
|
|
7
6
|
declare const __DEV__: boolean;
|
|
8
7
|
|
|
9
8
|
interface UsePaywallActionsProps {
|
|
10
9
|
userId?: string;
|
|
11
10
|
isAnonymous: boolean;
|
|
12
|
-
source?: "postOnboarding" | "inApp";
|
|
13
11
|
onPurchaseSuccess?: () => void;
|
|
14
12
|
onPurchaseError?: (error: string) => void;
|
|
15
13
|
onAuthRequired?: () => void;
|
|
@@ -19,7 +17,6 @@ interface UsePaywallActionsProps {
|
|
|
19
17
|
export const usePaywallActions = ({
|
|
20
18
|
userId,
|
|
21
19
|
isAnonymous,
|
|
22
|
-
source = "inApp",
|
|
23
20
|
onPurchaseSuccess,
|
|
24
21
|
onPurchaseError,
|
|
25
22
|
onAuthRequired,
|
|
@@ -27,12 +24,10 @@ export const usePaywallActions = ({
|
|
|
27
24
|
}: UsePaywallActionsProps) => {
|
|
28
25
|
const { mutateAsync: purchasePackage } = usePurchasePackage(userId);
|
|
29
26
|
const { mutateAsync: restorePurchases } = useRestorePurchase(userId);
|
|
30
|
-
const { pendingPackage, setPendingPurchase, clearPendingPurchase } = usePendingPurchase();
|
|
31
27
|
|
|
32
28
|
const handlePurchase = useCallback(async (pkg: PurchasesPackage) => {
|
|
33
29
|
if (isAnonymous) {
|
|
34
|
-
if (__DEV__) console.log("[PaywallActions] Anonymous user,
|
|
35
|
-
setPendingPurchase(pkg, source);
|
|
30
|
+
if (__DEV__) console.log("[PaywallActions] Anonymous user, redirecting to auth");
|
|
36
31
|
onAuthRequired?.();
|
|
37
32
|
return;
|
|
38
33
|
}
|
|
@@ -44,10 +39,11 @@ export const usePaywallActions = ({
|
|
|
44
39
|
onPurchaseSuccess?.();
|
|
45
40
|
onClose();
|
|
46
41
|
}
|
|
47
|
-
} catch (err:
|
|
48
|
-
|
|
42
|
+
} catch (err: unknown) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
onPurchaseError?.(message);
|
|
49
45
|
}
|
|
50
|
-
}, [isAnonymous,
|
|
46
|
+
}, [isAnonymous, purchasePackage, onClose, onPurchaseSuccess, onPurchaseError, onAuthRequired]);
|
|
51
47
|
|
|
52
48
|
const handleRestore = useCallback(async () => {
|
|
53
49
|
try {
|
|
@@ -57,10 +53,11 @@ export const usePaywallActions = ({
|
|
|
57
53
|
onPurchaseSuccess?.();
|
|
58
54
|
onClose();
|
|
59
55
|
}
|
|
60
|
-
} catch (err:
|
|
61
|
-
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
onPurchaseError?.(message);
|
|
62
59
|
}
|
|
63
60
|
}, [restorePurchases, onClose, onPurchaseSuccess, onPurchaseError]);
|
|
64
61
|
|
|
65
|
-
return { handlePurchase, handleRestore,
|
|
62
|
+
return { handlePurchase, handleRestore, purchasePackage };
|
|
66
63
|
};
|
|
@@ -8,8 +8,6 @@ export * from "./useDeductCredit";
|
|
|
8
8
|
export * from "./useInitializeCredits";
|
|
9
9
|
export * from "./useDevTestCallbacks";
|
|
10
10
|
export * from "./useFeatureGate";
|
|
11
|
-
export * from "./usePendingPurchase";
|
|
12
|
-
export * from "./useCompletePendingPurchase";
|
|
13
11
|
export * from "./usePaywallVisibility";
|
|
14
12
|
export * from "./usePremium";
|
|
15
13
|
export * from "./usePremiumGate";
|
|
@@ -58,6 +58,15 @@ export class PackageHandler {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log('[DEBUG PackageHandler] purchase() called', {
|
|
63
|
+
productId: pkg.product.identifier,
|
|
64
|
+
userId,
|
|
65
|
+
serviceExists: !!this.service,
|
|
66
|
+
isInitialized: this.service?.isInitialized(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
if (!this.service?.isInitialized()) {
|
|
62
71
|
if (__DEV__) {
|
|
63
72
|
console.log('[DEBUG PackageHandler] Service not initialized', {
|
|
@@ -68,7 +77,17 @@ export class PackageHandler {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
try {
|
|
80
|
+
if (__DEV__) {
|
|
81
|
+
console.log('[DEBUG PackageHandler] Calling service.purchasePackage...');
|
|
82
|
+
}
|
|
71
83
|
const result = await this.service.purchasePackage(pkg, userId);
|
|
84
|
+
if (__DEV__) {
|
|
85
|
+
console.log('[DEBUG PackageHandler] Purchase result:', {
|
|
86
|
+
success: result.success,
|
|
87
|
+
productId: pkg.product.identifier,
|
|
88
|
+
isPremium: result.isPremium,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
72
91
|
return result.success;
|
|
73
92
|
} catch (error) {
|
|
74
93
|
if (__DEV__) {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Coordinates UserIdProvider, InitializationCache, and PackageHandler
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
declare const __DEV__: boolean;
|
|
8
|
+
|
|
7
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
10
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
9
11
|
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
@@ -100,9 +102,24 @@ class SubscriptionManagerImpl {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
105
|
+
if (__DEV__) {
|
|
106
|
+
console.log('[DEBUG SubscriptionManager] purchasePackage called', {
|
|
107
|
+
productId: pkg.product.identifier,
|
|
108
|
+
isConfigured: this.isConfigured(),
|
|
109
|
+
isInitialized: this.isInitialized(),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
103
112
|
this.ensureConfigured();
|
|
104
113
|
const userId = this.initCache.getCurrentUserId();
|
|
105
|
-
if (
|
|
114
|
+
if (__DEV__) {
|
|
115
|
+
console.log('[DEBUG SubscriptionManager] userId from cache:', userId);
|
|
116
|
+
}
|
|
117
|
+
if (!userId) {
|
|
118
|
+
if (__DEV__) {
|
|
119
|
+
console.log('[DEBUG SubscriptionManager] No userId, returning false');
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
106
123
|
return this.packageHandler!.purchase(pkg, userId);
|
|
107
124
|
}
|
|
108
125
|
|
|
@@ -37,12 +37,25 @@ function isConsumableProduct(
|
|
|
37
37
|
/**
|
|
38
38
|
* Handle package purchase - supports both subscriptions and consumables
|
|
39
39
|
*/
|
|
40
|
+
declare const __DEV__: boolean;
|
|
41
|
+
|
|
40
42
|
export async function handlePurchase(
|
|
41
43
|
deps: PurchaseHandlerDeps,
|
|
42
44
|
pkg: PurchasesPackage,
|
|
43
45
|
userId: string
|
|
44
46
|
): Promise<PurchaseResult> {
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.log('[DEBUG PurchaseHandler] handlePurchase called', {
|
|
49
|
+
productId: pkg.product.identifier,
|
|
50
|
+
userId,
|
|
51
|
+
isInitialized: deps.isInitialized(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
if (!deps.isInitialized()) {
|
|
56
|
+
if (__DEV__) {
|
|
57
|
+
console.log('[DEBUG PurchaseHandler] Not initialized, throwing error');
|
|
58
|
+
}
|
|
46
59
|
throw new RevenueCatInitializationError();
|
|
47
60
|
}
|
|
48
61
|
|
|
@@ -50,10 +63,23 @@ export async function handlePurchase(
|
|
|
50
63
|
const isConsumable = isConsumableProduct(pkg, consumableIds);
|
|
51
64
|
|
|
52
65
|
try {
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
console.log('[DEBUG PurchaseHandler] Calling Purchases.purchasePackage...');
|
|
68
|
+
}
|
|
53
69
|
const purchaseResult = await Purchases.purchasePackage(pkg);
|
|
54
70
|
const customerInfo = purchaseResult.customerInfo;
|
|
55
71
|
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log('[DEBUG PurchaseHandler] Purchase completed', {
|
|
74
|
+
productId: pkg.product.identifier,
|
|
75
|
+
activeEntitlements: Object.keys(customerInfo.entitlements.active),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
56
79
|
if (isConsumable) {
|
|
80
|
+
if (__DEV__) {
|
|
81
|
+
console.log('[DEBUG PurchaseHandler] Consumable purchase SUCCESS');
|
|
82
|
+
}
|
|
57
83
|
await notifyPurchaseCompleted(
|
|
58
84
|
deps.config,
|
|
59
85
|
userId,
|
|
@@ -72,7 +98,18 @@ export async function handlePurchase(
|
|
|
72
98
|
const entitlementIdentifier = deps.config.entitlementIdentifier;
|
|
73
99
|
const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
|
|
74
100
|
|
|
101
|
+
if (__DEV__) {
|
|
102
|
+
console.log('[DEBUG PurchaseHandler] Checking premium status', {
|
|
103
|
+
entitlementIdentifier,
|
|
104
|
+
isPremium,
|
|
105
|
+
allEntitlements: customerInfo.entitlements.active,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
75
109
|
if (isPremium) {
|
|
110
|
+
if (__DEV__) {
|
|
111
|
+
console.log('[DEBUG PurchaseHandler] Premium purchase SUCCESS');
|
|
112
|
+
}
|
|
76
113
|
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
77
114
|
await notifyPurchaseCompleted(
|
|
78
115
|
deps.config,
|
|
@@ -86,6 +123,9 @@ export async function handlePurchase(
|
|
|
86
123
|
// In Preview API mode (Expo Go), purchases complete but entitlements aren't active
|
|
87
124
|
// Treat the purchase as successful for testing purposes
|
|
88
125
|
if (deps.isUsingTestStore()) {
|
|
126
|
+
if (__DEV__) {
|
|
127
|
+
console.log('[DEBUG PurchaseHandler] Test store purchase SUCCESS');
|
|
128
|
+
}
|
|
89
129
|
await notifyPurchaseCompleted(
|
|
90
130
|
deps.config,
|
|
91
131
|
userId,
|
|
@@ -95,15 +135,30 @@ export async function handlePurchase(
|
|
|
95
135
|
return { success: true, isPremium: false, customerInfo };
|
|
96
136
|
}
|
|
97
137
|
|
|
138
|
+
if (__DEV__) {
|
|
139
|
+
console.log('[DEBUG PurchaseHandler] Purchase FAILED - no entitlement');
|
|
140
|
+
}
|
|
98
141
|
throw new RevenueCatPurchaseError(
|
|
99
142
|
"Purchase completed but premium entitlement not active",
|
|
100
143
|
pkg.product.identifier
|
|
101
144
|
);
|
|
102
145
|
} catch (error) {
|
|
146
|
+
if (__DEV__) {
|
|
147
|
+
console.error('[DEBUG PurchaseHandler] Purchase error caught', {
|
|
148
|
+
error,
|
|
149
|
+
isUserCancelled: isUserCancelledError(error),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
103
152
|
if (isUserCancelledError(error)) {
|
|
153
|
+
if (__DEV__) {
|
|
154
|
+
console.log('[DEBUG PurchaseHandler] User cancelled');
|
|
155
|
+
}
|
|
104
156
|
return { success: false, isPremium: false };
|
|
105
157
|
}
|
|
106
158
|
const errorMessage = getErrorMessage(error, "Purchase failed");
|
|
159
|
+
if (__DEV__) {
|
|
160
|
+
console.error('[DEBUG PurchaseHandler] Throwing error:', errorMessage);
|
|
161
|
+
}
|
|
107
162
|
throw new RevenueCatPurchaseError(errorMessage, pkg.product.identifier);
|
|
108
163
|
}
|
|
109
164
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
8
8
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
|
+
import { useAlert } from "@umituz/react-native-design-system";
|
|
9
10
|
import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionManager";
|
|
10
11
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
11
12
|
import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
|
|
@@ -17,31 +18,6 @@ export interface PurchaseResult {
|
|
|
17
18
|
productId: string;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
-
* Global purchase lock to prevent double purchases
|
|
22
|
-
*/
|
|
23
|
-
let purchaseInProgress = false;
|
|
24
|
-
let purchaseProductId: string | null = null;
|
|
25
|
-
|
|
26
|
-
export const purchaseLock = {
|
|
27
|
-
isLocked: () => purchaseInProgress,
|
|
28
|
-
getProductId: () => purchaseProductId,
|
|
29
|
-
acquire: (productId: string): boolean => {
|
|
30
|
-
if (purchaseInProgress) {
|
|
31
|
-
if (__DEV__) {
|
|
32
|
-
console.log('[PurchaseLock] Already in progress:', purchaseProductId);
|
|
33
|
-
}
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
purchaseInProgress = true;
|
|
37
|
-
purchaseProductId = productId;
|
|
38
|
-
return true;
|
|
39
|
-
},
|
|
40
|
-
release: () => {
|
|
41
|
-
purchaseInProgress = false;
|
|
42
|
-
purchaseProductId = null;
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
21
|
|
|
46
22
|
/**
|
|
47
23
|
* Purchase a subscription package
|
|
@@ -49,6 +25,7 @@ export const purchaseLock = {
|
|
|
49
25
|
*/
|
|
50
26
|
export const usePurchasePackage = (userId: string | undefined) => {
|
|
51
27
|
const queryClient = useQueryClient();
|
|
28
|
+
const { showSuccess, showError } = useAlert();
|
|
52
29
|
|
|
53
30
|
return useMutation({
|
|
54
31
|
mutationFn: async (pkg: PurchasesPackage): Promise<PurchaseResult> => {
|
|
@@ -58,14 +35,6 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
58
35
|
|
|
59
36
|
const productId = pkg.product.identifier;
|
|
60
37
|
|
|
61
|
-
// Check and acquire purchase lock
|
|
62
|
-
if (!purchaseLock.acquire(productId)) {
|
|
63
|
-
if (__DEV__) {
|
|
64
|
-
console.log('[DEBUG usePurchasePackage] Skipping - purchase already in progress');
|
|
65
|
-
}
|
|
66
|
-
return { success: false, productId };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
38
|
if (__DEV__) {
|
|
70
39
|
console.log('[DEBUG usePurchasePackage] Starting purchase:', {
|
|
71
40
|
packageId: pkg.identifier,
|
|
@@ -74,34 +43,25 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
74
43
|
});
|
|
75
44
|
}
|
|
76
45
|
|
|
77
|
-
|
|
78
|
-
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
79
|
-
|
|
80
|
-
if (success) {
|
|
81
|
-
if (__DEV__) {
|
|
82
|
-
console.log('[DEBUG usePurchasePackage] Purchase successful:', {
|
|
83
|
-
packageId: pkg.identifier,
|
|
84
|
-
productId,
|
|
85
|
-
userId,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
// Credits will be initialized by CustomerInfoListener
|
|
89
|
-
} else {
|
|
90
|
-
if (__DEV__) {
|
|
91
|
-
console.log('[DEBUG usePurchasePackage] Purchase failed:', {
|
|
92
|
-
packageId: pkg.identifier,
|
|
93
|
-
userId,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
46
|
+
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
97
47
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
48
|
+
if (__DEV__) {
|
|
49
|
+
console.log('[DEBUG usePurchasePackage] Purchase result:', {
|
|
50
|
+
success,
|
|
51
|
+
packageId: pkg.identifier,
|
|
52
|
+
productId,
|
|
53
|
+
userId,
|
|
54
|
+
});
|
|
101
55
|
}
|
|
56
|
+
|
|
57
|
+
return { success, productId };
|
|
102
58
|
},
|
|
103
59
|
onSuccess: (result) => {
|
|
104
60
|
if (result.success) {
|
|
61
|
+
if (__DEV__) {
|
|
62
|
+
console.log('[DEBUG usePurchasePackage] onSuccess - invalidating queries');
|
|
63
|
+
}
|
|
64
|
+
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
105
65
|
queryClient.invalidateQueries({
|
|
106
66
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
107
67
|
});
|
|
@@ -110,15 +70,22 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
110
70
|
queryKey: creditsQueryKeys.user(userId),
|
|
111
71
|
});
|
|
112
72
|
}
|
|
73
|
+
} else {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.log('[DEBUG usePurchasePackage] onSuccess but result.success=false');
|
|
76
|
+
}
|
|
77
|
+
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
113
78
|
}
|
|
114
79
|
},
|
|
115
80
|
onError: (error) => {
|
|
116
81
|
if (__DEV__) {
|
|
117
|
-
console.error('[DEBUG usePurchasePackage]
|
|
82
|
+
console.error('[DEBUG usePurchasePackage] onError:', {
|
|
118
83
|
error,
|
|
119
84
|
userId: userId ?? "ANONYMOUS",
|
|
120
85
|
});
|
|
121
86
|
}
|
|
87
|
+
const message = error instanceof Error ? error.message : "An error occurred";
|
|
88
|
+
showError("Purchase Error", message);
|
|
122
89
|
},
|
|
123
90
|
});
|
|
124
91
|
};
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Complete Pending Purchase Hook
|
|
3
|
-
* Centralized hook for completing pending purchases after authentication
|
|
4
|
-
* This is the SINGLE source of truth for post-auth purchase completion
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useCallback, useRef, useEffect } from "react";
|
|
8
|
-
import { usePurchasePackage } from "../../revenuecat/presentation/hooks/usePurchasePackage";
|
|
9
|
-
import { usePendingPurchase, pendingPurchaseControl } from "./usePendingPurchase";
|
|
10
|
-
import { usePaywallVisibility } from "./usePaywallVisibility";
|
|
11
|
-
|
|
12
|
-
declare const __DEV__: boolean;
|
|
13
|
-
|
|
14
|
-
export interface UseCompletePendingPurchaseProps {
|
|
15
|
-
userId: string | undefined;
|
|
16
|
-
isAnonymous: boolean;
|
|
17
|
-
onPurchaseSuccess?: () => void;
|
|
18
|
-
onPurchaseError?: (error: string) => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface UseCompletePendingPurchaseResult {
|
|
22
|
-
completePendingPurchase: () => Promise<boolean>;
|
|
23
|
-
hasPendingPurchase: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function useCompletePendingPurchase({
|
|
27
|
-
userId,
|
|
28
|
-
isAnonymous,
|
|
29
|
-
onPurchaseSuccess,
|
|
30
|
-
onPurchaseError,
|
|
31
|
-
}: UseCompletePendingPurchaseProps): UseCompletePendingPurchaseResult {
|
|
32
|
-
const { mutateAsync: purchasePackage } = usePurchasePackage(userId);
|
|
33
|
-
const { clearPendingPurchase, hasPendingPurchase } = usePendingPurchase();
|
|
34
|
-
const { closePaywall } = usePaywallVisibility();
|
|
35
|
-
|
|
36
|
-
const wasAnonymousRef = useRef(isAnonymous);
|
|
37
|
-
const isProcessingRef = useRef(false);
|
|
38
|
-
|
|
39
|
-
const completePendingPurchase = useCallback(async (): Promise<boolean> => {
|
|
40
|
-
// Get current state directly to avoid stale closure
|
|
41
|
-
const currentState = pendingPurchaseControl.get();
|
|
42
|
-
|
|
43
|
-
if (!currentState.package) {
|
|
44
|
-
if (__DEV__) console.log("[CompletePendingPurchase] No pending package");
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!userId) {
|
|
49
|
-
if (__DEV__) console.log("[CompletePendingPurchase] No userId");
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (isProcessingRef.current) {
|
|
54
|
-
if (__DEV__) console.log("[CompletePendingPurchase] Already processing");
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
isProcessingRef.current = true;
|
|
59
|
-
const pkg = currentState.package;
|
|
60
|
-
const source = currentState.source;
|
|
61
|
-
|
|
62
|
-
if (__DEV__) {
|
|
63
|
-
console.log("[CompletePendingPurchase] Completing purchase:", {
|
|
64
|
-
identifier: pkg.product.identifier,
|
|
65
|
-
source,
|
|
66
|
-
userId,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Clear pending and close paywall BEFORE purchase to prevent double processing
|
|
71
|
-
clearPendingPurchase();
|
|
72
|
-
closePaywall();
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const result = await purchasePackage(pkg);
|
|
76
|
-
|
|
77
|
-
if (result.success) {
|
|
78
|
-
if (__DEV__) console.log("[CompletePendingPurchase] Purchase SUCCESS");
|
|
79
|
-
onPurchaseSuccess?.();
|
|
80
|
-
return true;
|
|
81
|
-
} else {
|
|
82
|
-
if (__DEV__) console.log("[CompletePendingPurchase] Purchase FAILED");
|
|
83
|
-
onPurchaseError?.("Purchase failed");
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
} catch (err: any) {
|
|
87
|
-
if (__DEV__) console.error("[CompletePendingPurchase] Purchase ERROR:", err);
|
|
88
|
-
onPurchaseError?.(err.message || String(err));
|
|
89
|
-
return false;
|
|
90
|
-
} finally {
|
|
91
|
-
isProcessingRef.current = false;
|
|
92
|
-
}
|
|
93
|
-
}, [userId, purchasePackage, clearPendingPurchase, closePaywall, onPurchaseSuccess, onPurchaseError]);
|
|
94
|
-
|
|
95
|
-
// Track auth state for reference (no auto-trigger - caller must explicitly call completePendingPurchase)
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
wasAnonymousRef.current = isAnonymous;
|
|
98
|
-
}, [isAnonymous]);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
completePendingPurchase,
|
|
102
|
-
hasPendingPurchase,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pending Purchase Hook
|
|
3
|
-
* Centralized global state for pending package purchase
|
|
4
|
-
* Used by both post-onboarding paywall and in-app paywall
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
8
|
-
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
|
-
|
|
10
|
-
type Listener = () => void;
|
|
11
|
-
|
|
12
|
-
interface PendingPurchaseState {
|
|
13
|
-
package: PurchasesPackage | null;
|
|
14
|
-
source: "postOnboarding" | "inApp" | null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let state: PendingPurchaseState = {
|
|
18
|
-
package: null,
|
|
19
|
-
source: null,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const listeners = new Set<Listener>();
|
|
23
|
-
|
|
24
|
-
const subscribe = (listener: Listener): (() => void) => {
|
|
25
|
-
listeners.add(listener);
|
|
26
|
-
return () => listeners.delete(listener);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const getSnapshot = (): PendingPurchaseState => state;
|
|
30
|
-
|
|
31
|
-
const setState = (newState: Partial<PendingPurchaseState>): void => {
|
|
32
|
-
state = { ...state, ...newState };
|
|
33
|
-
listeners.forEach((listener) => listener());
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Direct pending purchase control for non-React contexts
|
|
38
|
-
*/
|
|
39
|
-
export const pendingPurchaseControl = {
|
|
40
|
-
set: (pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => {
|
|
41
|
-
if (__DEV__) {
|
|
42
|
-
console.log("[PendingPurchase] Setting pending package:", {
|
|
43
|
-
identifier: pkg.product.identifier,
|
|
44
|
-
source,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
setState({ package: pkg, source });
|
|
48
|
-
},
|
|
49
|
-
clear: () => {
|
|
50
|
-
if (__DEV__) {
|
|
51
|
-
console.log("[PendingPurchase] Clearing pending package");
|
|
52
|
-
}
|
|
53
|
-
setState({ package: null, source: null });
|
|
54
|
-
},
|
|
55
|
-
get: () => state,
|
|
56
|
-
hasPending: () => state.package !== null,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export interface UsePendingPurchaseResult {
|
|
60
|
-
pendingPackage: PurchasesPackage | null;
|
|
61
|
-
pendingSource: "postOnboarding" | "inApp" | null;
|
|
62
|
-
setPendingPurchase: (pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => void;
|
|
63
|
-
clearPendingPurchase: () => void;
|
|
64
|
-
hasPendingPurchase: boolean;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function usePendingPurchase(): UsePendingPurchaseResult {
|
|
68
|
-
const currentState = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
69
|
-
|
|
70
|
-
const setPendingPurchase = useCallback(
|
|
71
|
-
(pkg: PurchasesPackage, source: "postOnboarding" | "inApp") => {
|
|
72
|
-
pendingPurchaseControl.set(pkg, source);
|
|
73
|
-
},
|
|
74
|
-
[]
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const clearPendingPurchase = useCallback(() => {
|
|
78
|
-
pendingPurchaseControl.clear();
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
pendingPackage: currentState.package,
|
|
83
|
-
pendingSource: currentState.source,
|
|
84
|
-
setPendingPurchase,
|
|
85
|
-
clearPendingPurchase,
|
|
86
|
-
hasPendingPurchase: currentState.package !== null,
|
|
87
|
-
};
|
|
88
|
-
}
|