@umituz/react-native-subscription 2.27.96 → 2.27.98
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/credits/core/Credits.ts +3 -11
- package/src/domains/credits/presentation/useDeductCredit.ts +0 -4
- package/src/domains/paywall/components/PaywallContainer.tsx +17 -1
- package/src/domains/paywall/components/PaywallContainer.types.ts +2 -1
- package/src/domains/paywall/components/PaywallModal.tsx +0 -6
- package/src/domains/paywall/hooks/usePaywallActions.ts +1 -1
- package/src/domains/subscription/application/SubscriptionInitializer.ts +0 -6
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +23 -12
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +4 -4
- package/src/domains/subscription/infrastructure/hooks/useCustomerInfo.ts +0 -19
- package/src/domains/subscription/infrastructure/hooks/useInitializeSubscription.ts +0 -16
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -31
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +0 -27
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -12
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +1 -7
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -2
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +0 -21
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +0 -9
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +20 -7
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +1 -16
- package/src/domains/subscription/presentation/screens/SubscriptionDetailScreen.tsx +49 -24
- package/src/domains/subscription/presentation/screens/components/CreditsList.tsx +14 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionActions.tsx +6 -1
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +20 -1
- package/src/domains/subscription/presentation/screens/components/UpgradePrompt.tsx +13 -1
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -3
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +1 -1
- package/src/domains/subscription/presentation/useFeatureGate.ts +9 -55
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -5
- package/src/init/index.ts +0 -3
- package/src/presentation/hooks/index.ts +0 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +27 -0
- package/src/utils/packageTypeDetector.ts +0 -4
- package/src/domains/subscription/presentation/screens/components/DevTestSection.tsx +0 -125
- package/src/domains/subscription/presentation/types/README.md +0 -22
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +0 -153
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +0 -74
- package/src/domains/subscription/presentation/useAuthSubscriptionSync.ts +0 -63
- package/src/domains/subscription/presentation/usePremiumGate.ts +0 -84
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +0 -148
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +0 -115
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +0 -57
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.98",
|
|
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",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
|
|
9
|
+
// Types imported from SubscriptionConstants are used directly in UserCredits interface
|
|
9
10
|
import type {
|
|
10
11
|
SubscriptionStatusType,
|
|
11
12
|
PeriodType,
|
|
@@ -15,15 +16,6 @@ import type {
|
|
|
15
16
|
PurchaseType
|
|
16
17
|
} from "../../subscription/core/SubscriptionConstants";
|
|
17
18
|
|
|
18
|
-
export type {
|
|
19
|
-
SubscriptionStatusType,
|
|
20
|
-
PeriodType,
|
|
21
|
-
PackageType,
|
|
22
|
-
Platform,
|
|
23
|
-
PurchaseSource,
|
|
24
|
-
PurchaseType
|
|
25
|
-
};
|
|
26
|
-
|
|
27
19
|
export type CreditType = "text" | "image";
|
|
28
20
|
|
|
29
21
|
/** Single Source of Truth for user subscription + credits data */
|
|
@@ -67,10 +59,10 @@ export interface CreditAllocation {
|
|
|
67
59
|
credits: number;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
export type PackageAllocationMap = Record<
|
|
62
|
+
export type PackageAllocationMap = Partial<Record<
|
|
71
63
|
Exclude<SubscriptionPackageType, "unknown">,
|
|
72
64
|
CreditAllocation
|
|
73
|
-
|
|
65
|
+
>>;
|
|
74
66
|
|
|
75
67
|
export interface CreditsConfig {
|
|
76
68
|
collectionName: string;
|
|
@@ -82,24 +82,20 @@ export const useDeductCredit = ({
|
|
|
82
82
|
|
|
83
83
|
const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
|
|
84
84
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
85
|
-
console.log("[useDeductCredit] Attempting to deduct:", cost);
|
|
86
85
|
}
|
|
87
86
|
try {
|
|
88
87
|
const res = await mutation.mutateAsync(cost);
|
|
89
88
|
if (!res.success) {
|
|
90
89
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
91
|
-
console.log("[useDeductCredit] Deduction failed:", res.error?.code, res.error?.message);
|
|
92
90
|
}
|
|
93
91
|
if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
|
|
94
92
|
return false;
|
|
95
93
|
}
|
|
96
94
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
|
-
console.log("[useDeductCredit] Deduction successful, remaining:", res.remainingCredits);
|
|
98
95
|
}
|
|
99
96
|
return true;
|
|
100
97
|
} catch (err) {
|
|
101
98
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
102
|
-
console.log("[useDeductCredit] Deduction error:", err);
|
|
103
99
|
}
|
|
104
100
|
return false;
|
|
105
101
|
}
|
|
@@ -49,9 +49,25 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
// Check trial eligibility only if trialConfig is enabled
|
|
52
|
+
// Use ref to track if we've already checked for these packages to avoid redundant calls
|
|
53
|
+
const checkedPackagesRef = React.useRef<string[]>([]);
|
|
54
|
+
|
|
52
55
|
useEffect(() => {
|
|
53
56
|
if (!trialConfig?.enabled) return;
|
|
54
57
|
if (packages.length === 0) return;
|
|
58
|
+
if (isLoading) return; // Wait for packages to fully load
|
|
59
|
+
|
|
60
|
+
// Get current package identifiers
|
|
61
|
+
const currentPackageIds = packages.map((pkg) => pkg.product.identifier);
|
|
62
|
+
const sortedIds = [...currentPackageIds].sort().join(",");
|
|
63
|
+
|
|
64
|
+
// Skip if we've already checked these exact packages
|
|
65
|
+
if (checkedPackagesRef.current.join(",") === sortedIds) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update ref
|
|
70
|
+
checkedPackagesRef.current = currentPackageIds;
|
|
55
71
|
|
|
56
72
|
// Get all actual product IDs from packages
|
|
57
73
|
const allProductIds = packages.map((pkg) => pkg.product.identifier);
|
|
@@ -72,7 +88,7 @@ export const PaywallContainer: React.FC<PaywallContainerProps> = (props) => {
|
|
|
72
88
|
if (productIdsToCheck.length > 0) {
|
|
73
89
|
checkEligibility(productIdsToCheck);
|
|
74
90
|
}
|
|
75
|
-
}, [packages, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
91
|
+
}, [packages, isLoading, checkEligibility, trialConfig?.enabled, trialConfig?.eligibleProductIds]);
|
|
76
92
|
|
|
77
93
|
// Convert eligibility map to format expected by PaywallModal
|
|
78
94
|
// Only process if trial is enabled
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ImageSourcePropType } from "react-native";
|
|
7
7
|
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
8
|
-
import type { PurchaseSource
|
|
8
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
9
|
+
import type { PackageAllocationMap } from "../../credits/core/Credits";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Trial display configuration
|
|
@@ -74,7 +74,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
74
74
|
if (!selectedPlanId || !onPurchase) return;
|
|
75
75
|
|
|
76
76
|
if (__DEV__) {
|
|
77
|
-
console.log("[PaywallModal] handlePurchase starting:", { selectedPlanId });
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
setIsLocalProcessing(true);
|
|
@@ -84,11 +83,9 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
84
83
|
const pkg = packages.find((p) => p.product.identifier === selectedPlanId);
|
|
85
84
|
if (pkg) {
|
|
86
85
|
if (__DEV__) {
|
|
87
|
-
console.log("[PaywallModal] Calling onPurchase:", { productId: pkg.product.identifier });
|
|
88
86
|
}
|
|
89
87
|
await onPurchase(pkg);
|
|
90
88
|
if (__DEV__) {
|
|
91
|
-
console.log("[PaywallModal] onPurchase completed");
|
|
92
89
|
}
|
|
93
90
|
}
|
|
94
91
|
} catch (error) {
|
|
@@ -100,7 +97,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
100
97
|
setIsLocalProcessing(false);
|
|
101
98
|
endPurchase();
|
|
102
99
|
if (__DEV__) {
|
|
103
|
-
console.log("[PaywallModal] handlePurchase finished");
|
|
104
100
|
}
|
|
105
101
|
}
|
|
106
102
|
}, [selectedPlanId, packages, onPurchase, startPurchase, endPurchase]);
|
|
@@ -109,14 +105,12 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
109
105
|
if (!onRestore || isProcessing) return;
|
|
110
106
|
|
|
111
107
|
if (__DEV__) {
|
|
112
|
-
console.log("[PaywallModal] handleRestore starting");
|
|
113
108
|
}
|
|
114
109
|
|
|
115
110
|
setIsLocalProcessing(true);
|
|
116
111
|
try {
|
|
117
112
|
await onRestore();
|
|
118
113
|
if (__DEV__) {
|
|
119
|
-
console.log("[PaywallModal] handleRestore completed");
|
|
120
114
|
}
|
|
121
115
|
} finally {
|
|
122
116
|
setIsLocalProcessing(false);
|
|
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|
|
2
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
3
3
|
import { useRestorePurchase } from "../../subscription/infrastructure/hooks/useRestorePurchase";
|
|
4
4
|
import { useAuthAwarePurchase } from "../../subscription/presentation/useAuthAwarePurchase";
|
|
5
|
-
import type { PurchaseSource } from "../../
|
|
5
|
+
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
6
6
|
|
|
7
7
|
interface UsePaywallActionsProps {
|
|
8
8
|
source?: PurchaseSource;
|
|
@@ -94,9 +94,6 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
94
94
|
|
|
95
95
|
const initializeInBackground = async (userId?: string): Promise<void> => {
|
|
96
96
|
await SubscriptionManager.initialize(userId);
|
|
97
|
-
if (__DEV__) {
|
|
98
|
-
console.log('[SubscriptionInitializer] Background init complete');
|
|
99
|
-
}
|
|
100
97
|
};
|
|
101
98
|
|
|
102
99
|
// 5. Start Background Init
|
|
@@ -110,9 +107,6 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
110
107
|
|
|
111
108
|
// 6. Listen for Auth Changes
|
|
112
109
|
setupAuthStateListener(() => auth, (newUserId) => {
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
|
|
115
|
-
}
|
|
116
110
|
initializeInBackground(newUserId);
|
|
117
111
|
});
|
|
118
112
|
};
|
|
@@ -25,21 +25,32 @@ export class PackageHandler {
|
|
|
25
25
|
|
|
26
26
|
async fetchPackages(): Promise<PurchasesPackage[]> {
|
|
27
27
|
if (!this.service.isInitialized()) {
|
|
28
|
-
throw new Error("Service not initialized");
|
|
28
|
+
throw new Error("Service not initialized. Please initialize before fetching packages.");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
try {
|
|
32
|
+
const offering = await this.service.fetchOfferings();
|
|
33
|
+
|
|
34
|
+
if (!offering) {
|
|
35
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const packages = offering.availablePackages;
|
|
40
|
+
if (!packages || packages.length === 0) {
|
|
41
|
+
// Return empty array instead of throwing - allows graceful degradation
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return packages;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
// Re-throw with more context
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to fetch subscription packages. ${
|
|
50
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
51
|
+
}`
|
|
52
|
+
);
|
|
35
53
|
}
|
|
36
|
-
|
|
37
|
-
const packages = offering.availablePackages;
|
|
38
|
-
if (!packages) {
|
|
39
|
-
throw new Error("No packages available in offering");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return packages;
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* TanStack Query keys and constants for subscription state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/** Query cache time constants */
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Query keys for TanStack Query
|
|
8
11
|
*/
|
|
@@ -12,7 +15,4 @@ export const SUBSCRIPTION_QUERY_KEYS = {
|
|
|
12
15
|
["subscription", "initialized", userId] as const,
|
|
13
16
|
} as const;
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
// This ensures users always see real-time subscription status
|
|
17
|
-
export const STALE_TIME = 0; // Always stale - refetch immediately
|
|
18
|
-
export const GC_TIME = 0; // Don't cache - garbage collect immediately
|
|
18
|
+
|
|
@@ -71,23 +71,10 @@ export function useCustomerInfo(): UseCustomerInfoResult {
|
|
|
71
71
|
const info = await Purchases.getCustomerInfo();
|
|
72
72
|
|
|
73
73
|
setCustomerInfo(info);
|
|
74
|
-
|
|
75
|
-
if (__DEV__) {
|
|
76
|
-
console.log('[DEBUG useCustomerInfo] Fetched:', {
|
|
77
|
-
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
78
|
-
latestExpiration: info.latestExpirationDate || "none",
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
74
|
} catch (err) {
|
|
82
75
|
const errorMessage =
|
|
83
76
|
err instanceof Error ? err.message : "Failed to fetch customer info";
|
|
84
77
|
setError(errorMessage);
|
|
85
|
-
|
|
86
|
-
if (__DEV__) {
|
|
87
|
-
console.error('[DEBUG useCustomerInfo] Error:', {
|
|
88
|
-
error: err,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
78
|
} finally {
|
|
92
79
|
setLoading(false);
|
|
93
80
|
setIsFetching(false);
|
|
@@ -100,12 +87,6 @@ export function useCustomerInfo(): UseCustomerInfoResult {
|
|
|
100
87
|
|
|
101
88
|
// Listen for real-time updates (renewals, purchases, restore)
|
|
102
89
|
const listener = (info: CustomerInfo) => {
|
|
103
|
-
if (__DEV__) {
|
|
104
|
-
console.log('[DEBUG useCustomerInfo] Listener update:', {
|
|
105
|
-
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
90
|
setCustomerInfo(info);
|
|
110
91
|
setError(null);
|
|
111
92
|
};
|
|
@@ -19,10 +19,6 @@ export const useInitializeSubscription = (userId: string | undefined) => {
|
|
|
19
19
|
throw new Error("User not authenticated");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if (__DEV__) {
|
|
23
|
-
console.log('[DEBUG useInitializeSubscription] Initializing:', { userId });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
return SubscriptionManager.initialize(userId);
|
|
27
23
|
},
|
|
28
24
|
onSuccess: () => {
|
|
@@ -30,18 +26,6 @@ export const useInitializeSubscription = (userId: string | undefined) => {
|
|
|
30
26
|
queryClient.invalidateQueries({
|
|
31
27
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
32
28
|
});
|
|
33
|
-
|
|
34
|
-
if (__DEV__) {
|
|
35
|
-
console.log('[DEBUG useInitializeSubscription] Success:', { userId });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
onError: (error) => {
|
|
40
|
-
if (__DEV__) {
|
|
41
|
-
console.error('[DEBUG useInitializeSubscription] Error:', {
|
|
42
|
-
error,
|
|
43
|
-
userId: userId ?? "ANONYMOUS",
|
|
44
|
-
});
|
|
45
29
|
}
|
|
46
30
|
},
|
|
47
31
|
});
|
|
@@ -40,33 +40,12 @@ export const usePurchasePackage = () => {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const productId = pkg.product.identifier;
|
|
43
|
-
|
|
44
|
-
if (__DEV__) {
|
|
45
|
-
console.log('[DEBUG usePurchasePackage] Starting purchase:', {
|
|
46
|
-
packageId: pkg.identifier,
|
|
47
|
-
productId,
|
|
48
|
-
userId,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
53
44
|
|
|
54
|
-
if (__DEV__) {
|
|
55
|
-
console.log('[DEBUG usePurchasePackage] Purchase result:', {
|
|
56
|
-
success,
|
|
57
|
-
packageId: pkg.identifier,
|
|
58
|
-
productId,
|
|
59
|
-
userId,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
45
|
return { success, productId };
|
|
64
46
|
},
|
|
65
47
|
onSuccess: (result) => {
|
|
66
48
|
if (result.success) {
|
|
67
|
-
if (__DEV__) {
|
|
68
|
-
console.log('[DEBUG usePurchasePackage] onSuccess - invalidating queries');
|
|
69
|
-
}
|
|
70
49
|
showSuccess("Purchase Successful", "Your subscription is now active!");
|
|
71
50
|
queryClient.invalidateQueries({
|
|
72
51
|
queryKey: SUBSCRIPTION_QUERY_KEYS.packages,
|
|
@@ -77,20 +56,10 @@ export const usePurchasePackage = () => {
|
|
|
77
56
|
});
|
|
78
57
|
}
|
|
79
58
|
} else {
|
|
80
|
-
if (__DEV__) {
|
|
81
|
-
console.log('[DEBUG usePurchasePackage] onSuccess but result.success=false');
|
|
82
|
-
}
|
|
83
59
|
showError("Purchase Failed", "Unable to complete purchase. Please try again.");
|
|
84
60
|
}
|
|
85
61
|
},
|
|
86
62
|
onError: (error) => {
|
|
87
|
-
if (__DEV__) {
|
|
88
|
-
console.error('[DEBUG usePurchasePackage] onError:', {
|
|
89
|
-
error,
|
|
90
|
-
userId: userId ?? "ANONYMOUS",
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
63
|
let title = "Purchase Error";
|
|
95
64
|
let message = "Unable to complete purchase. Please try again.";
|
|
96
65
|
|
|
@@ -34,26 +34,7 @@ export const useRestorePurchase = () => {
|
|
|
34
34
|
throw new Error("User not authenticated");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
if (__DEV__) {
|
|
38
|
-
console.log('[DEBUG useRestorePurchase] Starting restore:', { userId });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
37
|
const result = await SubscriptionManager.restore();
|
|
42
|
-
|
|
43
|
-
if (result.success) {
|
|
44
|
-
if (__DEV__) {
|
|
45
|
-
console.log('[DEBUG useRestorePurchase] Restore successful:', {
|
|
46
|
-
userId,
|
|
47
|
-
productId: result.productId,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
// Credits will be initialized by CustomerInfoListener
|
|
51
|
-
} else {
|
|
52
|
-
if (__DEV__) {
|
|
53
|
-
console.log('[DEBUG useRestorePurchase] Restore failed:', { userId });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
38
|
return result;
|
|
58
39
|
},
|
|
59
40
|
onSuccess: (result) => {
|
|
@@ -68,13 +49,5 @@ export const useRestorePurchase = () => {
|
|
|
68
49
|
}
|
|
69
50
|
}
|
|
70
51
|
},
|
|
71
|
-
onError: (error) => {
|
|
72
|
-
if (__DEV__) {
|
|
73
|
-
console.error('[DEBUG useRestorePurchase] Restore mutation failed:', {
|
|
74
|
-
error,
|
|
75
|
-
userId: userId ?? "ANONYMOUS",
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
52
|
});
|
|
80
53
|
};
|
|
@@ -85,9 +85,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
85
85
|
|
|
86
86
|
const service = getRevenueCatService();
|
|
87
87
|
if (!service || !service.isInitialized()) {
|
|
88
|
-
if (__DEV__) {
|
|
89
|
-
console.log("[TrialEligibility] RevenueCat not initialized");
|
|
90
|
-
}
|
|
91
88
|
return;
|
|
92
89
|
}
|
|
93
90
|
|
|
@@ -109,12 +106,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
109
106
|
eligible: isEligible,
|
|
110
107
|
trialDurationDays: 7, // Default to 7 days as configured in App Store Connect
|
|
111
108
|
};
|
|
112
|
-
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.log(
|
|
115
|
-
`[TrialEligibility] ${productId}: ${isEligible ? "ELIGIBLE" : "NOT_ELIGIBLE"}`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
109
|
}
|
|
119
110
|
|
|
120
111
|
// Update cache
|
|
@@ -127,9 +118,6 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
127
118
|
setEligibilityMap((prev) => ({ ...prev, ...newMap }));
|
|
128
119
|
}
|
|
129
120
|
} catch (error) {
|
|
130
|
-
if (__DEV__) {
|
|
131
|
-
console.log("[TrialEligibility] Error checking eligibility:", error);
|
|
132
|
-
}
|
|
133
121
|
// On error, default to eligible (better UX)
|
|
134
122
|
const fallbackMap: TrialEligibilityMap = {};
|
|
135
123
|
for (const productId of productIds) {
|
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
|
|
13
13
|
import {
|
|
14
14
|
SUBSCRIPTION_QUERY_KEYS,
|
|
15
|
-
STALE_TIME,
|
|
16
|
-
GC_TIME,
|
|
17
15
|
} from "./subscriptionQueryKeys";
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -41,11 +39,7 @@ export const useSubscriptionPackages = () => {
|
|
|
41
39
|
|
|
42
40
|
return SubscriptionManager.getPackages();
|
|
43
41
|
},
|
|
44
|
-
staleTime: STALE_TIME,
|
|
45
|
-
gcTime: GC_TIME,
|
|
46
42
|
enabled: isConfigured,
|
|
47
|
-
|
|
48
|
-
refetchOnWindowFocus: true, // Refetch when app becomes active
|
|
49
|
-
refetchOnReconnect: true, // Refetch when network reconnects
|
|
43
|
+
|
|
50
44
|
});
|
|
51
45
|
};
|
|
@@ -58,14 +58,6 @@ export class CustomerInfoListenerManager {
|
|
|
58
58
|
|
|
59
59
|
// Handle renewal (same product, extended expiration)
|
|
60
60
|
if (renewalResult.isRenewal && config.onRenewalDetected) {
|
|
61
|
-
if (__DEV__) {
|
|
62
|
-
console.log("[CustomerInfoListener] Renewal detected:", {
|
|
63
|
-
userId: this.currentUserId,
|
|
64
|
-
productId: renewalResult.productId,
|
|
65
|
-
newExpiration: renewalResult.newExpirationDate,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
try {
|
|
70
62
|
await config.onRenewalDetected(
|
|
71
63
|
this.currentUserId,
|
|
@@ -82,16 +74,6 @@ export class CustomerInfoListenerManager {
|
|
|
82
74
|
|
|
83
75
|
// Handle plan change (upgrade/downgrade)
|
|
84
76
|
if (renewalResult.isPlanChange && config.onPlanChanged) {
|
|
85
|
-
if (__DEV__) {
|
|
86
|
-
console.log("[CustomerInfoListener] Plan change detected:", {
|
|
87
|
-
userId: this.currentUserId,
|
|
88
|
-
previousProductId: renewalResult.previousProductId,
|
|
89
|
-
newProductId: renewalResult.productId,
|
|
90
|
-
isUpgrade: renewalResult.isUpgrade,
|
|
91
|
-
isDowngrade: renewalResult.isDowngrade,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
77
|
try {
|
|
96
78
|
await config.onPlanChanged(
|
|
97
79
|
this.currentUserId,
|
|
@@ -133,9 +115,6 @@ export class CustomerInfoListenerManager {
|
|
|
133
115
|
}
|
|
134
116
|
|
|
135
117
|
destroy(): void {
|
|
136
|
-
if (__DEV__) {
|
|
137
|
-
console.log('[CustomerInfoListenerManager] Destroying listener manager');
|
|
138
|
-
}
|
|
139
118
|
this.removeListener();
|
|
140
119
|
this.clearUserId();
|
|
141
120
|
// Reset renewal state to ensure clean state
|
|
@@ -85,15 +85,12 @@ export async function initializeSDK(
|
|
|
85
85
|
|
|
86
86
|
const key = apiKey || resolveApiKey(deps.config);
|
|
87
87
|
if (!key) {
|
|
88
|
-
if (__DEV__) console.log('[RevenueCat] No API key');
|
|
89
88
|
return { success: false, offering: null, isPremium: false };
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
configurationState.configurationInProgress = true;
|
|
93
92
|
try {
|
|
94
93
|
configureLogHandler();
|
|
95
|
-
if (__DEV__) console.log('[RevenueCat] Configuring:', key.substring(0, 10) + '...');
|
|
96
|
-
|
|
97
94
|
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
98
95
|
configurationState.isPurchasesConfigured = true;
|
|
99
96
|
deps.setInitialized(true);
|
|
@@ -104,14 +101,8 @@ export async function initializeSDK(
|
|
|
104
101
|
Purchases.getOfferings(),
|
|
105
102
|
]);
|
|
106
103
|
|
|
107
|
-
if (__DEV__) {
|
|
108
|
-
console.log('[RevenueCat] Initialized', {
|
|
109
|
-
packages: offerings.current?.availablePackages?.length ?? 0,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
104
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
113
105
|
} catch (error) {
|
|
114
|
-
if (__DEV__) console.error('[RevenueCat] Init failed:', error);
|
|
115
106
|
return { success: false, offering: null, isPremium: false };
|
|
116
107
|
} finally {
|
|
117
108
|
configurationState.configurationInProgress = false;
|
|
@@ -11,6 +11,8 @@ export class InitializationCache {
|
|
|
11
11
|
private initializationInProgress = false;
|
|
12
12
|
// Track which userId the promise is for (separate from currentUserId which is set after completion)
|
|
13
13
|
private promiseUserId: string | null = null;
|
|
14
|
+
// Track promise completion state to avoid returning failed promises
|
|
15
|
+
private promiseCompleted = true;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Atomically check if reinitialization is needed AND reserve the slot
|
|
@@ -22,17 +24,24 @@ export class InitializationCache {
|
|
|
22
24
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// If already initialized for this user and promise
|
|
26
|
-
if
|
|
27
|
+
// If already initialized for this user and promise completed successfully, return it
|
|
28
|
+
// Only return cached promise if it completed AND it's for the same user
|
|
29
|
+
if (this.initPromise && this.currentUserId === userId && !this.initializationInProgress && this.promiseCompleted) {
|
|
27
30
|
return { shouldInit: false, existingPromise: this.initPromise };
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
// Different user
|
|
31
|
-
// Atomically set the flag
|
|
32
|
-
this.initializationInProgress
|
|
33
|
-
|
|
33
|
+
// Different user, no initialization, or failed promise - need to reinitialize
|
|
34
|
+
// Atomically set the flag and clear previous state if needed
|
|
35
|
+
if (!this.initializationInProgress) {
|
|
36
|
+
this.initializationInProgress = true;
|
|
37
|
+
this.promiseUserId = userId;
|
|
38
|
+
this.promiseCompleted = false;
|
|
39
|
+
return { shouldInit: true, existingPromise: null };
|
|
40
|
+
}
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
// If we reach here, initialization is in progress for a different user
|
|
43
|
+
// Wait for current initialization to complete
|
|
44
|
+
return { shouldInit: false, existingPromise: this.initPromise };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
setPromise(promise: Promise<boolean>, userId: string): void {
|
|
@@ -45,6 +54,7 @@ export class InitializationCache {
|
|
|
45
54
|
if (result && this.promiseUserId === userId) {
|
|
46
55
|
this.currentUserId = userId;
|
|
47
56
|
}
|
|
57
|
+
this.promiseCompleted = true;
|
|
48
58
|
return result;
|
|
49
59
|
})
|
|
50
60
|
.catch(() => {
|
|
@@ -52,7 +62,9 @@ export class InitializationCache {
|
|
|
52
62
|
if (this.promiseUserId === userId) {
|
|
53
63
|
this.initPromise = null;
|
|
54
64
|
this.promiseUserId = null;
|
|
65
|
+
this.currentUserId = null; // Clear user on failure
|
|
55
66
|
}
|
|
67
|
+
this.promiseCompleted = true;
|
|
56
68
|
})
|
|
57
69
|
.finally(() => {
|
|
58
70
|
// Always release the mutex
|
|
@@ -71,5 +83,6 @@ export class InitializationCache {
|
|
|
71
83
|
this.currentUserId = null;
|
|
72
84
|
this.initializationInProgress = false;
|
|
73
85
|
this.promiseUserId = null;
|
|
86
|
+
this.promiseCompleted = true;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { CustomerInfo } from "react-native-purchases";
|
|
7
7
|
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
8
|
-
import type { PurchaseSource } from "../../../
|
|
8
|
+
import type { PurchaseSource } from "../../../subscription/core/SubscriptionConstants";
|
|
9
9
|
import { getPremiumEntitlement } from "../../core/RevenueCatTypes";
|
|
10
10
|
|
|
11
11
|
export async function syncPremiumStatus(
|
|
@@ -49,27 +49,12 @@ export async function notifyPurchaseCompleted(
|
|
|
49
49
|
customerInfo: CustomerInfo,
|
|
50
50
|
source?: PurchaseSource
|
|
51
51
|
): Promise<void> {
|
|
52
|
-
if (__DEV__) {
|
|
53
|
-
console.log('[PremiumStatusSyncer] notifyPurchaseCompleted called:', {
|
|
54
|
-
userId,
|
|
55
|
-
productId,
|
|
56
|
-
source,
|
|
57
|
-
hasCallback: !!config.onPurchaseCompleted,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
if (!config.onPurchaseCompleted) {
|
|
62
|
-
if (__DEV__) {
|
|
63
|
-
console.warn('[PremiumStatusSyncer] No onPurchaseCompleted callback configured!');
|
|
64
|
-
}
|
|
65
53
|
return;
|
|
66
54
|
}
|
|
67
55
|
|
|
68
56
|
try {
|
|
69
57
|
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
70
|
-
if (__DEV__) {
|
|
71
|
-
console.log('[PremiumStatusSyncer] onPurchaseCompleted callback executed successfully');
|
|
72
|
-
}
|
|
73
58
|
} catch (error) {
|
|
74
59
|
if (__DEV__) {
|
|
75
60
|
console.error('[PremiumStatusSyncer] onPurchaseCompleted callback failed:', error);
|