@umituz/react-native-subscription 2.27.113 → 2.27.114
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/application/CreditsInitializer.ts +27 -116
- package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -6
- package/src/domains/credits/application/creditDocumentHelpers.ts +58 -0
- package/src/domains/credits/application/creditOperationUtils.ts +154 -0
- package/src/domains/credits/presentation/useCredits.ts +1 -2
- package/src/domains/paywall/hooks/usePaywallActions.ts +0 -3
- package/src/domains/subscription/application/SubscriptionSyncService.ts +16 -20
- package/src/domains/subscription/core/RevenueCatError.ts +40 -31
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +0 -1
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +19 -85
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +33 -75
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +57 -0
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +3 -12
- package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +0 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +2 -4
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +1 -5
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -12
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +69 -0
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +77 -0
- package/src/domains/subscription/presentation/components/feedback/FeedbackOption.tsx +139 -0
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +15 -70
- package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -92
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +1 -18
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +19 -69
- package/src/domains/subscription/presentation/usePremium.ts +2 -11
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -6
- package/src/domains/trial/application/TrialService.ts +4 -8
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +1 -1
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -13
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -10
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -8
- package/src/init/createSubscriptionInitModule.ts +1 -4
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -14
- package/src/shared/application/FeedbackService.ts +0 -21
- package/src/shared/infrastructure/SubscriptionEventBus.ts +1 -2
- package/src/shared/types/CommonTypes.ts +65 -0
- package/src/shared/utils/BaseError.ts +26 -0
- package/src/shared/utils/Logger.ts +14 -45
- package/src/shared/utils/Result.ts +16 -0
- package/src/shared/utils/SubscriptionError.ts +20 -30
- package/src/utils/packageTypeDetector.ts +0 -4
|
@@ -5,24 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import { getRevenueCatService } from "../services/RevenueCatService";
|
|
9
|
+
import {
|
|
10
|
+
checkTrialEligibility,
|
|
11
|
+
createFallbackEligibilityMap,
|
|
12
|
+
hasAnyEligibleTrial,
|
|
13
|
+
type ProductTrialEligibility,
|
|
14
|
+
type TrialEligibilityMap,
|
|
15
|
+
} from "../utils/trialEligibilityUtils";
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
export interface ProductTrialEligibility {
|
|
16
|
-
/** Product identifier */
|
|
17
|
-
productId: string;
|
|
18
|
-
/** Whether eligible for introductory offer (free trial) */
|
|
19
|
-
eligible: boolean;
|
|
20
|
-
/** Trial duration in days (if available from product) */
|
|
21
|
-
trialDurationDays?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Map of product ID to eligibility */
|
|
25
|
-
export type TrialEligibilityMap = Record<string, ProductTrialEligibility>;
|
|
17
|
+
export type { ProductTrialEligibility, TrialEligibilityMap };
|
|
26
18
|
|
|
27
19
|
export interface UseRevenueCatTrialEligibilityResult {
|
|
28
20
|
/** Map of product IDs to their trial eligibility */
|
|
@@ -37,17 +29,6 @@ export interface UseRevenueCatTrialEligibilityResult {
|
|
|
37
29
|
getProductEligibility: (productId: string) => ProductTrialEligibility | null;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
/** Cache duration in milliseconds (5 minutes) */
|
|
41
|
-
const CACHE_DURATION_MS = 5 * 60 * 1000;
|
|
42
|
-
|
|
43
|
-
/** Cached eligibility result */
|
|
44
|
-
interface CachedEligibility {
|
|
45
|
-
data: TrialEligibilityMap;
|
|
46
|
-
timestamp: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let eligibilityCache: CachedEligibility | null = null;
|
|
50
|
-
|
|
51
32
|
/**
|
|
52
33
|
* Hook to check trial eligibility via RevenueCat
|
|
53
34
|
* Uses Apple's introductory offer eligibility system
|
|
@@ -56,6 +37,7 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
56
37
|
const [eligibilityMap, setEligibilityMap] = useState<TrialEligibilityMap>({});
|
|
57
38
|
const [isLoading, setIsLoading] = useState(false);
|
|
58
39
|
const isMountedRef = useRef(true);
|
|
40
|
+
const currentRequestRef = useRef<number | null>(null);
|
|
59
41
|
|
|
60
42
|
useEffect(() => {
|
|
61
43
|
isMountedRef.current = true;
|
|
@@ -69,69 +51,29 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
69
51
|
return;
|
|
70
52
|
}
|
|
71
53
|
|
|
72
|
-
// Check cache validity
|
|
73
|
-
if (
|
|
74
|
-
eligibilityCache &&
|
|
75
|
-
Date.now() - eligibilityCache.timestamp < CACHE_DURATION_MS
|
|
76
|
-
) {
|
|
77
|
-
const allCached = productIds.every(
|
|
78
|
-
(id) => eligibilityCache?.data[id] !== undefined
|
|
79
|
-
);
|
|
80
|
-
if (allCached && isMountedRef.current) {
|
|
81
|
-
setEligibilityMap(eligibilityCache.data);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
54
|
const service = getRevenueCatService();
|
|
87
55
|
if (!service || !service.isInitialized()) {
|
|
88
56
|
return;
|
|
89
57
|
}
|
|
90
58
|
|
|
59
|
+
const requestId = Date.now();
|
|
60
|
+
currentRequestRef.current = requestId;
|
|
91
61
|
setIsLoading(true);
|
|
92
62
|
|
|
93
63
|
try {
|
|
94
|
-
const
|
|
95
|
-
await Purchases.checkTrialOrIntroductoryPriceEligibility(productIds);
|
|
96
|
-
|
|
97
|
-
const newMap: TrialEligibilityMap = {};
|
|
64
|
+
const newMap = await checkTrialEligibility(productIds);
|
|
98
65
|
|
|
99
|
-
|
|
100
|
-
const eligibility = eligibilities[productId];
|
|
101
|
-
const isEligible =
|
|
102
|
-
eligibility?.status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE;
|
|
103
|
-
|
|
104
|
-
newMap[productId] = {
|
|
105
|
-
productId,
|
|
106
|
-
eligible: isEligible,
|
|
107
|
-
trialDurationDays: 7, // Default to 7 days as configured in App Store Connect
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Update cache
|
|
112
|
-
eligibilityCache = {
|
|
113
|
-
data: { ...eligibilityCache?.data, ...newMap },
|
|
114
|
-
timestamp: Date.now(),
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if (isMountedRef.current) {
|
|
66
|
+
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
118
67
|
setEligibilityMap((prev) => ({ ...prev, ...newMap }));
|
|
119
68
|
}
|
|
120
69
|
} catch {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
fallbackMap[productId] = {
|
|
125
|
-
productId,
|
|
126
|
-
eligible: true,
|
|
127
|
-
trialDurationDays: 7,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
if (isMountedRef.current) {
|
|
70
|
+
const fallbackMap = createFallbackEligibilityMap(productIds);
|
|
71
|
+
|
|
72
|
+
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
131
73
|
setEligibilityMap((prev) => ({ ...prev, ...fallbackMap }));
|
|
132
74
|
}
|
|
133
75
|
} finally {
|
|
134
|
-
if (isMountedRef.current) {
|
|
76
|
+
if (isMountedRef.current && currentRequestRef.current === requestId) {
|
|
135
77
|
setIsLoading(false);
|
|
136
78
|
}
|
|
137
79
|
}
|
|
@@ -144,9 +86,7 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
144
86
|
[eligibilityMap]
|
|
145
87
|
);
|
|
146
88
|
|
|
147
|
-
const hasEligibleTrial =
|
|
148
|
-
(e) => e.eligible
|
|
149
|
-
);
|
|
89
|
+
const hasEligibleTrial = hasAnyEligibleTrial(eligibilityMap);
|
|
150
90
|
|
|
151
91
|
return {
|
|
152
92
|
eligibilityMap,
|
|
@@ -157,9 +97,3 @@ export function useRevenueCatTrialEligibility(): UseRevenueCatTrialEligibilityRe
|
|
|
157
97
|
};
|
|
158
98
|
}
|
|
159
99
|
|
|
160
|
-
/**
|
|
161
|
-
* Clear eligibility cache (useful for testing)
|
|
162
|
-
*/
|
|
163
|
-
export function clearTrialEligibilityCache(): void {
|
|
164
|
-
eligibilityCache = null;
|
|
165
|
-
}
|
|
@@ -10,6 +10,12 @@ import { initializeRevenueCatService, getRevenueCatService } from "../services/R
|
|
|
10
10
|
import { PackageHandler } from "../handlers/PackageHandler";
|
|
11
11
|
import type { PremiumStatus, RestoreResultInfo } from "../handlers/PackageHandler";
|
|
12
12
|
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
13
|
+
import {
|
|
14
|
+
ensureConfigured,
|
|
15
|
+
getCurrentUserIdOrThrow,
|
|
16
|
+
getOrCreateService,
|
|
17
|
+
ensureServiceAvailable,
|
|
18
|
+
} from "./subscriptionManagerUtils";
|
|
13
19
|
|
|
14
20
|
export interface SubscriptionManagerConfig {
|
|
15
21
|
config: RevenueCatConfig;
|
|
@@ -33,28 +39,17 @@ class SubscriptionManagerImpl {
|
|
|
33
39
|
return;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!this.managerConfig) {
|
|
41
|
-
throw new Error("Manager not configured");
|
|
42
|
-
}
|
|
42
|
+
ensureServiceAvailable(this.serviceInstance);
|
|
43
|
+
ensureConfigured(this.managerConfig);
|
|
43
44
|
|
|
44
45
|
this.packageHandler = new PackageHandler(
|
|
45
|
-
this.serviceInstance
|
|
46
|
-
this.managerConfig
|
|
46
|
+
this.serviceInstance!,
|
|
47
|
+
this.managerConfig!.config.entitlementIdentifier
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
private ensureConfigured(): void {
|
|
51
|
-
if (!this.managerConfig) {
|
|
52
|
-
throw new Error("SubscriptionManager not configured");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
51
|
async initialize(userId?: string): Promise<boolean> {
|
|
57
|
-
this.
|
|
52
|
+
ensureConfigured(this.managerConfig);
|
|
58
53
|
|
|
59
54
|
const actualUserId = userId ?? (await this.managerConfig!.getAnonymousUserId());
|
|
60
55
|
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(actualUserId);
|
|
@@ -67,12 +62,10 @@ class SubscriptionManagerImpl {
|
|
|
67
62
|
await initializeRevenueCatService(this.managerConfig!.config);
|
|
68
63
|
this.serviceInstance = getRevenueCatService();
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
throw new Error("Service instance not available after initialization");
|
|
72
|
-
}
|
|
73
|
-
|
|
65
|
+
ensureServiceAvailable(this.serviceInstance);
|
|
74
66
|
this.ensurePackageHandlerInitialized();
|
|
75
|
-
|
|
67
|
+
|
|
68
|
+
const result = await this.serviceInstance!.initialize(actualUserId);
|
|
76
69
|
return result.success;
|
|
77
70
|
})();
|
|
78
71
|
|
|
@@ -81,11 +74,7 @@ class SubscriptionManagerImpl {
|
|
|
81
74
|
}
|
|
82
75
|
|
|
83
76
|
isInitializedForUser(userId: string): boolean {
|
|
84
|
-
if (!this.serviceInstance) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!this.serviceInstance.isInitialized()) {
|
|
77
|
+
if (!this.serviceInstance?.isInitialized()) {
|
|
89
78
|
return false;
|
|
90
79
|
}
|
|
91
80
|
|
|
@@ -93,57 +82,35 @@ class SubscriptionManagerImpl {
|
|
|
93
82
|
}
|
|
94
83
|
|
|
95
84
|
async getPackages(): Promise<PurchasesPackage[]> {
|
|
96
|
-
this.
|
|
97
|
-
|
|
98
|
-
if (!this.serviceInstance) {
|
|
99
|
-
this.serviceInstance = getRevenueCatService();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!this.serviceInstance) {
|
|
103
|
-
throw new Error("Service instance not available");
|
|
104
|
-
}
|
|
105
|
-
|
|
85
|
+
ensureConfigured(this.managerConfig);
|
|
86
|
+
this.serviceInstance = getOrCreateService(this.serviceInstance);
|
|
106
87
|
this.ensurePackageHandlerInitialized();
|
|
88
|
+
|
|
107
89
|
return this.packageHandler!.fetchPackages();
|
|
108
90
|
}
|
|
109
91
|
|
|
110
92
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
const userId = this.state.initCache.getCurrentUserId();
|
|
114
|
-
if (!userId) {
|
|
115
|
-
throw new Error("No current user found");
|
|
116
|
-
}
|
|
117
|
-
|
|
93
|
+
ensureConfigured(this.managerConfig);
|
|
94
|
+
const userId = getCurrentUserIdOrThrow(this.state);
|
|
118
95
|
this.ensurePackageHandlerInitialized();
|
|
96
|
+
|
|
119
97
|
return this.packageHandler!.purchase(pkg, userId);
|
|
120
98
|
}
|
|
121
99
|
|
|
122
100
|
async restore(): Promise<RestoreResultInfo> {
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
const userId = this.state.initCache.getCurrentUserId();
|
|
126
|
-
if (!userId) {
|
|
127
|
-
throw new Error("No current user found");
|
|
128
|
-
}
|
|
129
|
-
|
|
101
|
+
ensureConfigured(this.managerConfig);
|
|
102
|
+
const userId = getCurrentUserIdOrThrow(this.state);
|
|
130
103
|
this.ensurePackageHandlerInitialized();
|
|
104
|
+
|
|
131
105
|
return this.packageHandler!.restore(userId);
|
|
132
106
|
}
|
|
133
107
|
|
|
134
108
|
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
135
|
-
this.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!userId) {
|
|
139
|
-
throw new Error("No current user found");
|
|
140
|
-
}
|
|
109
|
+
ensureConfigured(this.managerConfig);
|
|
110
|
+
getCurrentUserIdOrThrow(this.state);
|
|
111
|
+
ensureServiceAvailable(this.serviceInstance);
|
|
141
112
|
|
|
142
|
-
|
|
143
|
-
throw new Error("Service instance not available");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const customerInfo = await this.serviceInstance.getCustomerInfo();
|
|
113
|
+
const customerInfo = await this.serviceInstance!.getCustomerInfo();
|
|
147
114
|
|
|
148
115
|
if (!customerInfo) {
|
|
149
116
|
throw new Error("Customer info not available");
|
|
@@ -154,12 +121,10 @@ class SubscriptionManagerImpl {
|
|
|
154
121
|
}
|
|
155
122
|
|
|
156
123
|
async reset(): Promise<void> {
|
|
157
|
-
|
|
158
|
-
await this.serviceInstance.reset();
|
|
159
|
-
}
|
|
160
|
-
|
|
124
|
+
await this.serviceInstance?.reset();
|
|
161
125
|
this.state.reset();
|
|
162
126
|
this.serviceInstance = null;
|
|
127
|
+
this.packageHandler = null;
|
|
163
128
|
}
|
|
164
129
|
|
|
165
130
|
isConfigured(): boolean {
|
|
@@ -167,19 +132,12 @@ class SubscriptionManagerImpl {
|
|
|
167
132
|
}
|
|
168
133
|
|
|
169
134
|
isInitialized(): boolean {
|
|
170
|
-
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return this.serviceInstance.isInitialized();
|
|
135
|
+
return this.serviceInstance?.isInitialized() ?? false;
|
|
175
136
|
}
|
|
176
137
|
|
|
177
138
|
getEntitlementId(): string {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return this.managerConfig.config.entitlementIdentifier;
|
|
139
|
+
ensureConfigured(this.managerConfig);
|
|
140
|
+
return this.managerConfig!.config.entitlementIdentifier;
|
|
183
141
|
}
|
|
184
142
|
}
|
|
185
143
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Manager Utilities
|
|
3
|
+
* Validation and helper functions for SubscriptionManager
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SubscriptionManagerConfig } from "./SubscriptionManager";
|
|
7
|
+
import type { IRevenueCatService } from "../../../../shared/application/ports/IRevenueCatService";
|
|
8
|
+
import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate that manager is configured
|
|
12
|
+
*/
|
|
13
|
+
export function ensureConfigured(config: SubscriptionManagerConfig | null): void {
|
|
14
|
+
if (!config) {
|
|
15
|
+
throw new Error("SubscriptionManager not configured");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get current user ID or throw
|
|
21
|
+
*/
|
|
22
|
+
export function getCurrentUserIdOrThrow(state: SubscriptionInternalState): string {
|
|
23
|
+
const userId = state.initCache.getCurrentUserId();
|
|
24
|
+
if (!userId) {
|
|
25
|
+
throw new Error("No current user found");
|
|
26
|
+
}
|
|
27
|
+
return userId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get service instance or initialize
|
|
32
|
+
*/
|
|
33
|
+
export function getOrCreateService(
|
|
34
|
+
currentInstance: IRevenueCatService | null
|
|
35
|
+
): IRevenueCatService {
|
|
36
|
+
if (currentInstance) {
|
|
37
|
+
return currentInstance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { getRevenueCatService } = require("../services/RevenueCatService");
|
|
41
|
+
const serviceInstance = getRevenueCatService();
|
|
42
|
+
|
|
43
|
+
if (!serviceInstance) {
|
|
44
|
+
throw new Error("Service instance not available");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return serviceInstance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validate service is available
|
|
52
|
+
*/
|
|
53
|
+
export function ensureServiceAvailable(service: IRevenueCatService | null): void {
|
|
54
|
+
if (!service) {
|
|
55
|
+
throw new Error("Service instance not available");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -65,10 +65,7 @@ export class CustomerInfoListenerManager {
|
|
|
65
65
|
renewalResult.newExpirationDate!,
|
|
66
66
|
customerInfo
|
|
67
67
|
);
|
|
68
|
-
} catch
|
|
69
|
-
if (__DEV__) {
|
|
70
|
-
console.error("[CustomerInfoListener] Renewal callback failed:", error);
|
|
71
|
-
}
|
|
68
|
+
} catch {
|
|
72
69
|
}
|
|
73
70
|
}
|
|
74
71
|
|
|
@@ -82,10 +79,7 @@ export class CustomerInfoListenerManager {
|
|
|
82
79
|
renewalResult.isUpgrade,
|
|
83
80
|
customerInfo
|
|
84
81
|
);
|
|
85
|
-
} catch
|
|
86
|
-
if (__DEV__) {
|
|
87
|
-
console.error("[CustomerInfoListener] Plan change callback failed:", error);
|
|
88
|
-
}
|
|
82
|
+
} catch {
|
|
89
83
|
}
|
|
90
84
|
}
|
|
91
85
|
|
|
@@ -96,10 +90,7 @@ export class CustomerInfoListenerManager {
|
|
|
96
90
|
if (!renewalResult.isRenewal && !renewalResult.isPlanChange) {
|
|
97
91
|
try {
|
|
98
92
|
await syncPremiumStatus(config, this.currentUserId, customerInfo);
|
|
99
|
-
} catch
|
|
100
|
-
if (__DEV__) {
|
|
101
|
-
console.error("[CustomerInfoListener] syncPremiumStatus failed:", error);
|
|
102
|
-
}
|
|
93
|
+
} catch {
|
|
103
94
|
}
|
|
104
95
|
}
|
|
105
96
|
};
|
|
@@ -29,8 +29,6 @@ export async function handlePurchase(
|
|
|
29
29
|
const entitlementIdentifier = deps.config.entitlementIdentifier;
|
|
30
30
|
|
|
31
31
|
try {
|
|
32
|
-
if (__DEV__) console.log('[Purchase] Starting:', pkg.product.identifier);
|
|
33
|
-
|
|
34
32
|
const { customerInfo } = await Purchases.purchasePackage(pkg);
|
|
35
33
|
const savedPurchase = getSavedPurchase();
|
|
36
34
|
const source = savedPurchase?.source;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Purchases, {
|
|
1
|
+
import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
|
|
2
2
|
import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
|
|
3
3
|
import type { RevenueCatConfig } from "../../core/RevenueCatConfig";
|
|
4
4
|
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
@@ -25,14 +25,12 @@ function configureLogHandler(): void {
|
|
|
25
25
|
if (configurationState.isLogHandlerConfigured) return;
|
|
26
26
|
if (typeof Purchases.setLogHandler !== 'function') return;
|
|
27
27
|
try {
|
|
28
|
-
Purchases.setLogHandler((
|
|
28
|
+
Purchases.setLogHandler((_logLevel, message) => {
|
|
29
29
|
const ignoreMessages = ['Purchase was cancelled', 'AppTransaction', "Couldn't find previous transactions"];
|
|
30
30
|
if (ignoreMessages.some(m => message.includes(m))) return;
|
|
31
|
-
if (logLevel === LOG_LEVEL.ERROR && __DEV__) console.error('[RevenueCat]', message);
|
|
32
31
|
});
|
|
33
32
|
configurationState.isLogHandlerConfigured = true;
|
|
34
33
|
} catch {
|
|
35
|
-
// Native module not available (Expo Go)
|
|
36
34
|
}
|
|
37
35
|
}
|
|
38
36
|
|
|
@@ -108,11 +108,7 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
108
108
|
try {
|
|
109
109
|
await Purchases.logOut();
|
|
110
110
|
this.stateManager.setInitialized(false);
|
|
111
|
-
} catch
|
|
112
|
-
// Log error for debugging but don't throw - reset is cleanup operation
|
|
113
|
-
if (__DEV__) {
|
|
114
|
-
console.error('[RevenueCatService] Reset failed:', error);
|
|
115
|
-
}
|
|
111
|
+
} catch {
|
|
116
112
|
}
|
|
117
113
|
}
|
|
118
114
|
}
|
|
@@ -35,10 +35,7 @@ export async function syncPremiumStatus(
|
|
|
35
35
|
} else {
|
|
36
36
|
await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined, undefined);
|
|
37
37
|
}
|
|
38
|
-
} catch
|
|
39
|
-
if (__DEV__) {
|
|
40
|
-
console.error('[PremiumStatusSyncer] syncPremiumStatus failed:', error);
|
|
41
|
-
}
|
|
38
|
+
} catch {
|
|
42
39
|
}
|
|
43
40
|
}
|
|
44
41
|
|
|
@@ -55,10 +52,7 @@ export async function notifyPurchaseCompleted(
|
|
|
55
52
|
|
|
56
53
|
try {
|
|
57
54
|
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
58
|
-
} catch
|
|
59
|
-
if (__DEV__) {
|
|
60
|
-
console.error('[PremiumStatusSyncer] onPurchaseCompleted callback failed:', error);
|
|
61
|
-
}
|
|
55
|
+
} catch {
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
58
|
|
|
@@ -74,9 +68,6 @@ export async function notifyRestoreCompleted(
|
|
|
74
68
|
|
|
75
69
|
try {
|
|
76
70
|
await config.onRestoreCompleted(userId, isPremium, customerInfo);
|
|
77
|
-
} catch
|
|
78
|
-
if (__DEV__) {
|
|
79
|
-
console.error('[PremiumStatusSyncer] notifyRestoreCompleted failed:', error);
|
|
80
|
-
}
|
|
71
|
+
} catch {
|
|
81
72
|
}
|
|
82
73
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Purchase State Manager
|
|
3
|
+
* Manages global state for auth-aware purchase operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
import type { PurchaseSource } from "../../core/SubscriptionConstants";
|
|
8
|
+
|
|
9
|
+
export interface PurchaseAuthProvider {
|
|
10
|
+
isAuthenticated: () => boolean;
|
|
11
|
+
showAuthModal: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SavedPurchaseState {
|
|
15
|
+
pkg: PurchasesPackage;
|
|
16
|
+
source: PurchaseSource;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SAVED_PURCHASE_EXPIRY_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
class AuthPurchaseStateManager {
|
|
23
|
+
private authProvider: PurchaseAuthProvider | null = null;
|
|
24
|
+
private savedPurchaseState: SavedPurchaseState | null = null;
|
|
25
|
+
|
|
26
|
+
configure(provider: PurchaseAuthProvider): void {
|
|
27
|
+
this.authProvider = provider;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getProvider(): PurchaseAuthProvider | null {
|
|
31
|
+
return this.authProvider;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
savePurchase(pkg: PurchasesPackage, source: PurchaseSource): void {
|
|
35
|
+
this.savedPurchaseState = {
|
|
36
|
+
pkg,
|
|
37
|
+
source,
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getSavedPurchase(): { pkg: PurchasesPackage; source: PurchaseSource } | null {
|
|
43
|
+
if (!this.savedPurchaseState) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const isExpired = Date.now() - this.savedPurchaseState.timestamp > SAVED_PURCHASE_EXPIRY_MS;
|
|
48
|
+
if (isExpired) {
|
|
49
|
+
this.savedPurchaseState = null;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
pkg: this.savedPurchaseState.pkg,
|
|
55
|
+
source: this.savedPurchaseState.source,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clearSavedPurchase(): void {
|
|
60
|
+
this.savedPurchaseState = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cleanup(): void {
|
|
64
|
+
this.authProvider = null;
|
|
65
|
+
this.savedPurchaseState = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const authPurchaseStateManager = new AuthPurchaseStateManager();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trial Eligibility Utilities
|
|
3
|
+
* Business logic for checking trial eligibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Purchases, {
|
|
7
|
+
type IntroEligibility,
|
|
8
|
+
INTRO_ELIGIBILITY_STATUS,
|
|
9
|
+
} from "react-native-purchases";
|
|
10
|
+
|
|
11
|
+
/** Trial eligibility info for a single product */
|
|
12
|
+
export interface ProductTrialEligibility {
|
|
13
|
+
productId: string;
|
|
14
|
+
eligible: boolean;
|
|
15
|
+
trialDurationDays?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Map of product ID to eligibility */
|
|
19
|
+
export type TrialEligibilityMap = Record<string, ProductTrialEligibility>;
|
|
20
|
+
|
|
21
|
+
/** Default trial duration in days */
|
|
22
|
+
const DEFAULT_TRIAL_DURATION_DAYS = 7;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check trial eligibility for product IDs
|
|
26
|
+
*/
|
|
27
|
+
export async function checkTrialEligibility(
|
|
28
|
+
productIds: string[]
|
|
29
|
+
): Promise<TrialEligibilityMap> {
|
|
30
|
+
const eligibilities: Record<string, IntroEligibility> =
|
|
31
|
+
await Purchases.checkTrialOrIntroductoryPriceEligibility(productIds);
|
|
32
|
+
|
|
33
|
+
const result: TrialEligibilityMap = {};
|
|
34
|
+
|
|
35
|
+
for (const productId of productIds) {
|
|
36
|
+
const eligibility = eligibilities[productId];
|
|
37
|
+
const isEligible =
|
|
38
|
+
eligibility?.status === INTRO_ELIGIBILITY_STATUS.INTRO_ELIGIBILITY_STATUS_ELIGIBLE;
|
|
39
|
+
|
|
40
|
+
result[productId] = {
|
|
41
|
+
productId,
|
|
42
|
+
eligible: isEligible,
|
|
43
|
+
trialDurationDays: DEFAULT_TRIAL_DURATION_DAYS,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create fallback eligibility map (all eligible)
|
|
52
|
+
* Used when eligibility check fails
|
|
53
|
+
*/
|
|
54
|
+
export function createFallbackEligibilityMap(
|
|
55
|
+
productIds: string[]
|
|
56
|
+
): TrialEligibilityMap {
|
|
57
|
+
const result: TrialEligibilityMap = {};
|
|
58
|
+
|
|
59
|
+
for (const productId of productIds) {
|
|
60
|
+
result[productId] = {
|
|
61
|
+
productId,
|
|
62
|
+
eligible: true,
|
|
63
|
+
trialDurationDays: DEFAULT_TRIAL_DURATION_DAYS,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if any product has eligible trial
|
|
72
|
+
*/
|
|
73
|
+
export function hasAnyEligibleTrial(
|
|
74
|
+
eligibilityMap: TrialEligibilityMap
|
|
75
|
+
): boolean {
|
|
76
|
+
return Object.values(eligibilityMap).some((e) => e.eligible);
|
|
77
|
+
}
|