@umituz/react-native-subscription 2.27.113 → 2.27.115

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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditsInitializer.ts +27 -116
  3. package/src/domains/credits/application/credit-strategies/CreditAllocationOrchestrator.ts +1 -6
  4. package/src/domains/credits/application/creditDocumentHelpers.ts +58 -0
  5. package/src/domains/credits/application/creditOperationUtils.ts +154 -0
  6. package/src/domains/credits/presentation/useCredits.ts +1 -2
  7. package/src/domains/paywall/hooks/usePaywallActions.ts +0 -3
  8. package/src/domains/subscription/application/SubscriptionSyncService.ts +19 -20
  9. package/src/domains/subscription/core/RevenueCatError.ts +40 -31
  10. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +0 -1
  11. package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +19 -85
  12. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +33 -75
  13. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +57 -0
  14. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +6 -12
  15. package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +0 -2
  16. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +3 -4
  17. package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +2 -5
  18. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +6 -12
  19. package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +69 -0
  20. package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +77 -0
  21. package/src/domains/subscription/presentation/components/feedback/FeedbackOption.tsx +139 -0
  22. package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +15 -70
  23. package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -92
  24. package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +1 -18
  25. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +22 -76
  26. package/src/domains/subscription/presentation/usePremium.ts +2 -11
  27. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +1 -6
  28. package/src/domains/trial/application/TrialService.ts +4 -8
  29. package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -13
  30. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -10
  31. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -8
  32. package/src/init/createSubscriptionInitModule.ts +1 -4
  33. package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -14
  34. package/src/shared/application/FeedbackService.ts +0 -21
  35. package/src/shared/infrastructure/SubscriptionEventBus.ts +2 -2
  36. package/src/shared/types/CommonTypes.ts +65 -0
  37. package/src/shared/utils/BaseError.ts +26 -0
  38. package/src/shared/utils/Logger.ts +14 -45
  39. package/src/shared/utils/SubscriptionError.ts +20 -30
  40. 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 Purchases, {
9
- type IntroEligibility,
10
- INTRO_ELIGIBILITY_STATUS,
11
- } from "react-native-purchases";
12
- import { getRevenueCatService } from "../../infrastructure/services/RevenueCatService";
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
- /** Trial eligibility info for a single product */
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 eligibilities: Record<string, IntroEligibility> =
95
- await Purchases.checkTrialOrIntroductoryPriceEligibility(productIds);
96
-
97
- const newMap: TrialEligibilityMap = {};
64
+ const newMap = await checkTrialEligibility(productIds);
98
65
 
99
- for (const productId of productIds) {
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
- // On error, default to eligible (better UX)
122
- const fallbackMap: TrialEligibilityMap = {};
123
- for (const productId of productIds) {
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 = Object.values(eligibilityMap).some(
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
- if (!this.serviceInstance) {
37
- throw new Error("Service instance not available");
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.config.entitlementIdentifier
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.ensureConfigured();
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
- if (!this.serviceInstance) {
71
- throw new Error("Service instance not available after initialization");
72
- }
73
-
65
+ ensureServiceAvailable(this.serviceInstance);
74
66
  this.ensurePackageHandlerInitialized();
75
- const result = await this.serviceInstance.initialize(actualUserId);
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.ensureConfigured();
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.ensureConfigured();
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.ensureConfigured();
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.ensureConfigured();
136
-
137
- const userId = this.state.initCache.getCurrentUserId();
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
- if (!this.serviceInstance) {
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
- if (this.serviceInstance) {
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
- if (!this.serviceInstance) {
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
- if (!this.managerConfig) {
179
- throw new Error("SubscriptionManager not configured");
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,8 @@ export class CustomerInfoListenerManager {
65
65
  renewalResult.newExpirationDate!,
66
66
  customerInfo
67
67
  );
68
- } catch (error) {
69
- if (__DEV__) {
70
- console.error("[CustomerInfoListener] Renewal callback failed:", error);
71
- }
68
+ } catch {
69
+ // Silently fail listener callbacks to prevent crashing the main listener
72
70
  }
73
71
  }
74
72
 
@@ -82,10 +80,8 @@ export class CustomerInfoListenerManager {
82
80
  renewalResult.isUpgrade,
83
81
  customerInfo
84
82
  );
85
- } catch (error) {
86
- if (__DEV__) {
87
- console.error("[CustomerInfoListener] Plan change callback failed:", error);
88
- }
83
+ } catch {
84
+ // Silently fail listener callbacks to prevent crashing the main listener
89
85
  }
90
86
  }
91
87
 
@@ -96,10 +92,8 @@ export class CustomerInfoListenerManager {
96
92
  if (!renewalResult.isRenewal && !renewalResult.isPlanChange) {
97
93
  try {
98
94
  await syncPremiumStatus(config, this.currentUserId, customerInfo);
99
- } catch (error) {
100
- if (__DEV__) {
101
- console.error("[CustomerInfoListener] syncPremiumStatus failed:", error);
102
- }
95
+ } catch {
96
+ // Silently fail listener callbacks to prevent crashing the main listener
103
97
  }
104
98
  }
105
99
  };
@@ -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, { LOG_LEVEL, type CustomerInfo, type PurchasesOfferings } from "react-native-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,13 @@ function configureLogHandler(): void {
25
25
  if (configurationState.isLogHandlerConfigured) return;
26
26
  if (typeof Purchases.setLogHandler !== 'function') return;
27
27
  try {
28
- Purchases.setLogHandler((logLevel, message) => {
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)
34
+ // Failing to set log handler should not block initialization
36
35
  }
37
36
  }
38
37
 
@@ -108,11 +108,8 @@ export class RevenueCatService implements IRevenueCatService {
108
108
  try {
109
109
  await Purchases.logOut();
110
110
  this.stateManager.setInitialized(false);
111
- } catch (error) {
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 {
112
+ // Silently fail during logout to allow cleanup to complete
116
113
  }
117
114
  }
118
115
  }
@@ -35,10 +35,8 @@ export async function syncPremiumStatus(
35
35
  } else {
36
36
  await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined, undefined);
37
37
  }
38
- } catch (error) {
39
- if (__DEV__) {
40
- console.error('[PremiumStatusSyncer] syncPremiumStatus failed:', error);
41
- }
38
+ } catch {
39
+ // Silently fail callback notifications to prevent crashing the main flow
42
40
  }
43
41
  }
44
42
 
@@ -55,10 +53,8 @@ export async function notifyPurchaseCompleted(
55
53
 
56
54
  try {
57
55
  await config.onPurchaseCompleted(userId, productId, customerInfo, source);
58
- } catch (error) {
59
- if (__DEV__) {
60
- console.error('[PremiumStatusSyncer] onPurchaseCompleted callback failed:', error);
61
- }
56
+ } catch {
57
+ // Silently fail callback notifications to prevent crashing the main flow
62
58
  }
63
59
  }
64
60
 
@@ -74,9 +70,7 @@ export async function notifyRestoreCompleted(
74
70
 
75
71
  try {
76
72
  await config.onRestoreCompleted(userId, isPremium, customerInfo);
77
- } catch (error) {
78
- if (__DEV__) {
79
- console.error('[PremiumStatusSyncer] notifyRestoreCompleted failed:', error);
80
- }
73
+ } catch {
74
+ // Silently fail callback notifications to prevent crashing the main flow
81
75
  }
82
76
  }
@@ -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
+ }