@umituz/react-native-subscription 2.35.15 → 2.35.17

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 (45) hide show
  1. package/package.json +1 -1
  2. package/src/domains/config/utils/planSelectors.ts +5 -1
  3. package/src/domains/credits/presentation/useCredits.ts +6 -5
  4. package/src/domains/paywall/hooks/usePaywallActions.ts +2 -82
  5. package/src/domains/revenuecat/core/customerInfoHelpers.ts +21 -0
  6. package/src/domains/subscription/application/SubscriptionAuthListener.ts +0 -19
  7. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +1 -1
  8. package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +2 -8
  9. package/src/domains/subscription/application/initializer/ConfigValidator.ts +2 -2
  10. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +23 -3
  11. package/src/domains/subscription/application/statusChangeHandlers.ts +0 -30
  12. package/src/domains/subscription/constants/thresholds.ts +10 -0
  13. package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +3 -3
  14. package/src/domains/subscription/infrastructure/handlers/package-operations/PackageFetcher.ts +0 -19
  15. package/src/domains/subscription/infrastructure/hooks/customer-info/useCustomerInfo.ts +1 -1
  16. package/src/domains/subscription/infrastructure/hooks/useInitializeSubscription.ts +2 -4
  17. package/src/domains/subscription/infrastructure/hooks/usePaywallFlow.ts +12 -2
  18. package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -44
  19. package/src/domains/subscription/infrastructure/hooks/useRevenueCat.ts +23 -5
  20. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +1 -31
  21. package/src/domains/subscription/infrastructure/services/OfferingsFetcher.ts +0 -21
  22. package/src/domains/subscription/infrastructure/services/listeners/CustomerInfoHandler.ts +6 -36
  23. package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +0 -6
  24. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -44
  25. package/src/domains/subscription/presentation/featureGateActions.ts +0 -37
  26. package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +1 -1
  27. package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +1 -1
  28. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +0 -43
  29. package/src/domains/subscription/presentation/useFeatureGate.ts +1 -40
  30. package/src/domains/subscription/presentation/useSubscriptionStatus.ts +6 -20
  31. package/src/domains/subscription/utils/authGuards.ts +26 -2
  32. package/src/domains/subscription/utils/expirationHelpers.ts +2 -2
  33. package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
  34. package/src/domains/trial/application/TrialService.ts +12 -4
  35. package/src/domains/wallet/infrastructure/repositories/transaction/TransactionWriter.ts +1 -1
  36. package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +2 -5
  37. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +2 -5
  38. package/src/shared/infrastructure/react-query/hooks/usePreviousUserCleanup.ts +39 -0
  39. package/src/shared/infrastructure/react-query/queryConfig.ts +22 -0
  40. package/src/shared/infrastructure/react-query/queryInvalidation.ts +46 -0
  41. package/src/shared/presentation/hooks/useServiceCall.ts +2 -1
  42. package/src/shared/utils/errorUtils.ts +32 -0
  43. package/src/utils/appUtils.ts +6 -0
  44. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.constants.ts +0 -1
  45. package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.constants.ts +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.35.15",
3
+ "version": "2.35.17",
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",
@@ -32,7 +32,11 @@ export const getCreditLimitForPlan = (
32
32
  planId: string
33
33
  ): number => {
34
34
  const plan = getPlanById(config, planId);
35
- return plan?.credits ?? 0;
35
+ if (!plan) {
36
+
37
+ return 0;
38
+ }
39
+ return plan.credits;
36
40
  };
37
41
 
38
42
  export const determinePlanFromCredits = (
@@ -2,6 +2,8 @@ import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
2
2
  import { useCallback, useMemo, useEffect } from "react";
3
3
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
4
4
  import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
5
+ import { NO_CACHE_QUERY_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
6
+ import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
5
7
  import {
6
8
  getCreditsRepository,
7
9
  getCreditsConfig,
@@ -44,15 +46,14 @@ export const useCredits = (): UseCreditsResult => {
44
46
  return result.data ?? null;
45
47
  },
46
48
  enabled: queryEnabled,
47
- gcTime: 10 * 60 * 1000,
48
- staleTime: 30 * 1000,
49
- refetchOnMount: true,
50
- refetchOnWindowFocus: false,
51
- refetchOnReconnect: true,
49
+ ...NO_CACHE_QUERY_CONFIG,
52
50
  });
53
51
 
54
52
  const queryClient = useQueryClient();
55
53
 
54
+ // Clean up previous user's cache on logout/user switch
55
+ usePreviousUserCleanup(userId, queryClient, creditsQueryKeys.user);
56
+
56
57
  useEffect(() => {
57
58
  if (!isAuthenticated(userId)) return undefined;
58
59
 
@@ -7,8 +7,6 @@ import type { PurchasesPackage } from "react-native-purchases";
7
7
  import { usePurchaseLoadingStore } from "../../subscription/presentation/stores";
8
8
  import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
9
9
 
10
- declare const __DEV__: boolean;
11
-
12
10
  export interface UsePaywallActionsParams {
13
11
  packages?: PurchasesPackage[];
14
12
  onPurchase?: (pkg: PurchasesPackage) => Promise<void | boolean>;
@@ -54,35 +52,18 @@ export function usePaywallActions({
54
52
  });
55
53
 
56
54
  const handlePurchase = useCallback(async () => {
57
- if (typeof __DEV__ !== "undefined" && __DEV__) {
58
- console.log("[usePaywallActions] handlePurchase called", {
59
- selectedPlanId,
60
- hasOnPurchase: !!onPurchaseRef.current,
61
- isProcessing,
62
- packagesCount: packages.length,
63
- });
64
- }
65
55
 
66
56
  if (!selectedPlanId) {
67
- if (typeof __DEV__ !== "undefined" && __DEV__) {
68
- console.warn("[usePaywallActions] ❌ No plan selected");
69
- }
70
57
  return;
71
58
  }
72
59
 
73
60
  if (!onPurchaseRef.current) {
74
- if (typeof __DEV__ !== "undefined" && __DEV__) {
75
- console.error("[usePaywallActions] ❌ No onPurchase callback provided");
76
- }
77
61
  const err = new Error("Purchase handler not configured");
78
62
  onPurchaseErrorRef.current?.(err);
79
63
  return;
80
64
  }
81
65
 
82
66
  if (isProcessing) {
83
- if (typeof __DEV__ !== "undefined" && __DEV__) {
84
- console.warn("[usePaywallActions] ⚠️ Already processing, ignoring duplicate request");
85
- }
86
67
  return;
87
68
  }
88
69
 
@@ -100,117 +81,56 @@ export function usePaywallActions({
100
81
  return;
101
82
  }
102
83
 
103
- if (typeof __DEV__ !== "undefined" && __DEV__) {
104
- console.log("[usePaywallActions] ✅ Starting purchase", {
105
- productId: pkg.product.identifier,
106
- title: pkg.product.title,
107
- });
108
- }
109
-
110
84
  setIsLocalProcessing(true);
111
85
  startPurchase(selectedPlanId, "manual");
112
86
 
113
87
  try {
114
- if (typeof __DEV__ !== "undefined" && __DEV__) {
115
- console.log("[usePaywallActions] 🚀 Calling onPurchase callback");
116
- }
117
88
 
118
89
  const success = await onPurchaseRef.current(pkg);
119
90
 
120
- if (typeof __DEV__ !== "undefined" && __DEV__) {
121
- console.log("[usePaywallActions] 📦 Purchase result:", { success, type: typeof success });
122
- }
123
-
124
91
  if (success === true) {
125
- if (typeof __DEV__ !== "undefined" && __DEV__) {
126
- console.log("[usePaywallActions] ✅ Purchase successful, calling success callbacks");
127
- }
128
92
  onPurchaseSuccessRef.current?.();
129
93
  onCloseRef.current?.();
130
94
  } else if (success === false) {
131
95
  if (typeof __DEV__ !== "undefined" && __DEV__) {
132
96
  console.warn("[usePaywallActions] ⚠️ Purchase returned false (user cancelled or failed)");
133
97
  }
134
- } else {
135
- if (typeof __DEV__ !== "undefined" && __DEV__) {
136
- console.error("[usePaywallActions] ❌ Purchase returned unexpected value:", success);
137
- }
138
98
  }
99
+ // else: success is undefined/null - no action needed
139
100
  } catch (error) {
140
- if (typeof __DEV__ !== "undefined" && __DEV__) {
141
- console.error("[usePaywallActions] ❌ Purchase error:", error);
142
- }
143
101
  const err = error instanceof Error ? error : new Error(String(error));
144
102
  onPurchaseErrorRef.current?.(err);
145
103
  } finally {
146
- if (typeof __DEV__ !== "undefined" && __DEV__) {
147
- console.log("[usePaywallActions] 🏁 Purchase completed, cleaning up");
148
- }
149
104
  setIsLocalProcessing(false);
150
105
  endPurchase(selectedPlanId);
151
106
  }
152
107
  }, [selectedPlanId, packages, isProcessing, startPurchase, endPurchase]);
153
108
 
154
109
  const handleRestore = useCallback(async () => {
155
- if (typeof __DEV__ !== "undefined" && __DEV__) {
156
- console.log("[usePaywallActions] handleRestore called", {
157
- hasOnRestore: !!onRestoreRef.current,
158
- isProcessing,
159
- });
160
- }
161
110
 
162
111
  if (!onRestoreRef.current) {
163
- if (typeof __DEV__ !== "undefined" && __DEV__) {
164
- console.error("[usePaywallActions] ❌ No onRestore callback provided");
165
- }
166
112
  const err = new Error("Restore handler not configured");
167
113
  onPurchaseErrorRef.current?.(err);
168
114
  return;
169
115
  }
170
116
 
171
117
  if (isProcessing) {
172
- if (typeof __DEV__ !== "undefined" && __DEV__) {
173
- console.warn("[usePaywallActions] ⚠️ Already processing, ignoring restore request");
174
- }
175
118
  return;
176
119
  }
177
120
 
178
121
  setIsLocalProcessing(true);
179
122
  try {
180
- if (typeof __DEV__ !== "undefined" && __DEV__) {
181
- console.log("[usePaywallActions] 🚀 Calling onRestore callback");
182
- }
183
123
 
184
124
  const success = await onRestoreRef.current();
185
125
 
186
- if (typeof __DEV__ !== "undefined" && __DEV__) {
187
- console.log("[usePaywallActions] 📦 Restore result:", { success, type: typeof success });
188
- }
189
-
190
126
  if (success === true) {
191
- if (typeof __DEV__ !== "undefined" && __DEV__) {
192
- console.log("[usePaywallActions] ✅ Restore successful");
193
- }
194
127
  onPurchaseSuccessRef.current?.();
195
- } else if (success === false) {
196
- if (typeof __DEV__ !== "undefined" && __DEV__) {
197
- console.warn("[usePaywallActions] ⚠️ Restore returned false");
198
- }
199
- } else {
200
- if (typeof __DEV__ !== "undefined" && __DEV__) {
201
- console.error("[usePaywallActions] ❌ Restore returned unexpected value:", success);
202
- }
203
128
  }
129
+ // else: success is false/undefined - restore failed or user cancelled, no action needed
204
130
  } catch (error) {
205
- if (typeof __DEV__ !== "undefined" && __DEV__) {
206
- console.error("[usePaywallActions] ❌ Restore error:", error);
207
- }
208
131
  const err = error instanceof Error ? error : new Error(String(error));
209
132
  onPurchaseErrorRef.current?.(err);
210
133
  } finally {
211
- if (typeof __DEV__ !== "undefined" && __DEV__) {
212
- console.log("[usePaywallActions] 🏁 Restore completed");
213
- }
214
134
  setIsLocalProcessing(false);
215
135
  }
216
136
  }, [isProcessing]);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Customer Info Helper Functions
3
+ * Utilities for extracting data from RevenueCat CustomerInfo objects
4
+ */
5
+
6
+ import type { CustomerInfo } from "react-native-purchases";
7
+
8
+ /**
9
+ * Extracts active entitlement IDs from CustomerInfo
10
+ * Useful for logging and debugging
11
+ *
12
+ * @param customerInfo - RevenueCat CustomerInfo object
13
+ * @returns Array of active entitlement IDs
14
+ *
15
+ * @example
16
+ * const activeIds = getActiveEntitlementIds(customerInfo);
17
+ * console.log("Active entitlements:", activeIds); // ["premium", "pro_features"]
18
+ */
19
+ export function getActiveEntitlementIds(customerInfo: CustomerInfo): string[] {
20
+ return Object.keys(customerInfo.entitlements.active);
21
+ }
@@ -1,7 +1,5 @@
1
1
  import type { FirebaseAuthLike } from "./SubscriptionInitializerTypes";
2
2
 
3
- declare const __DEV__: boolean;
4
-
5
3
  /**
6
4
  * Gets the current user ID from Firebase auth.
7
5
  * Returns undefined for anonymous users to let RevenueCat generate its own anonymous ID.
@@ -9,17 +7,11 @@ declare const __DEV__: boolean;
9
7
  export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string | undefined => {
10
8
  const auth = getAuth();
11
9
  if (!auth) {
12
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
13
- console.log('[SubscriptionAuthListener] No auth available');
14
- }
15
10
  return undefined;
16
11
  }
17
12
 
18
13
  const user = auth.currentUser;
19
14
  if (!user) {
20
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
- console.log('[SubscriptionAuthListener] No current user');
22
- }
23
15
  return undefined;
24
16
  }
25
17
 
@@ -30,10 +22,6 @@ export const getCurrentUserId = (getAuth: () => FirebaseAuthLike | null): string
30
22
  return undefined;
31
23
  }
32
24
 
33
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
34
- console.log('[SubscriptionAuthListener] Authenticated user:', user.uid);
35
- }
36
-
37
25
  return user.uid;
38
26
  };
39
27
 
@@ -48,16 +36,9 @@ export const setupAuthStateListener = (
48
36
  ): (() => void) | null => {
49
37
  const auth = getAuth();
50
38
  if (!auth) {
51
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
52
- console.log('[SubscriptionAuthListener] Cannot setup listener - no auth available');
53
- }
54
39
  return null;
55
40
  }
56
41
 
57
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
58
- console.log('[SubscriptionAuthListener] Setting up auth state listener');
59
- }
60
-
61
42
  return auth.onAuthStateChanged((user) => {
62
43
  const userId = (user && !user.isAnonymous) ? user.uid : undefined;
63
44
 
@@ -43,7 +43,7 @@ export class SubscriptionSyncProcessor {
43
43
 
44
44
  async processRenewal(userId: string, productId: string, newExpirationDate: string, customerInfo: CustomerInfo) {
45
45
  const revenueCatData = extractRevenueCatData(customerInfo, this.entitlementId);
46
- revenueCatData.expirationDate = newExpirationDate || revenueCatData.expirationDate;
46
+ revenueCatData.expirationDate = newExpirationDate ?? revenueCatData.expirationDate;
47
47
  const purchaseId = generateRenewalId(revenueCatData.originalTransactionId, productId, newExpirationDate);
48
48
 
49
49
  const creditsUserId = await this.getCreditsUserId(userId);
@@ -2,8 +2,6 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
2
2
  import { getCurrentUserId, setupAuthStateListener } from "../SubscriptionAuthListener";
3
3
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
4
 
5
- declare const __DEV__: boolean;
6
-
7
5
  export async function startBackgroundInitialization(config: SubscriptionInitConfig): Promise<() => void> {
8
6
  const initializeInBackground = async (revenueCatUserId?: string): Promise<void> => {
9
7
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
@@ -17,10 +15,6 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
17
15
  throw new Error("Firebase auth is not available");
18
16
  }
19
17
 
20
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
21
- console.log('[BackgroundInitializer] Starting background initialization');
22
- }
23
-
24
18
  const initialRevenueCatUserId = getCurrentUserId(() => auth);
25
19
 
26
20
  if (typeof __DEV__ !== 'undefined' && __DEV__) {
@@ -35,8 +29,8 @@ export async function startBackgroundInitialization(config: SubscriptionInitConf
35
29
  }
36
30
  try {
37
31
  await initializeInBackground(newRevenueCatUserId);
38
- } catch (error) {
39
- console.error('[BackgroundInitializer] Failed to reinitialize on auth change', { userId: newRevenueCatUserId, error });
32
+ } catch (_error) {
33
+ // Background re-initialization errors are non-critical, already logged by SubscriptionManager
40
34
  }
41
35
  });
42
36
 
@@ -4,8 +4,8 @@ import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
4
4
  export function getApiKey(config: SubscriptionInitConfig): string {
5
5
  const { apiKey, apiKeyIos, apiKeyAndroid } = config;
6
6
  const key = Platform.OS === 'ios'
7
- ? (apiKeyIos || apiKey)
8
- : (apiKeyAndroid || apiKey);
7
+ ? (apiKeyIos ?? apiKey)
8
+ : (apiKeyAndroid ?? apiKey);
9
9
 
10
10
  if (!key) {
11
11
  throw new Error('API key required');
@@ -3,6 +3,8 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
3
3
  import { configureAuthProvider } from "../../presentation/useAuthAwarePurchase";
4
4
  import { SubscriptionSyncService } from "../SubscriptionSyncService";
5
5
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
6
+ import type { CustomerInfo } from "react-native-purchases";
7
+ import type { PackageType } from "../../../revenuecat/core/types/RevenueCatTypes";
6
8
 
7
9
  export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
8
10
  const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated, getAnonymousUserId } = config;
@@ -23,9 +25,27 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
23
25
  apiKey,
24
26
  entitlementIdentifier: entitlementId,
25
27
  consumableProductIdentifiers: [creditPackages.identifierPattern],
26
- onPurchaseCompleted: (u: string, p: string, c: any, s: any, pkgType: any) => syncService.handlePurchase(u, p, c, s, pkgType),
27
- onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
28
- onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
28
+ onPurchaseCompleted: (
29
+ u: string,
30
+ p: string,
31
+ c: CustomerInfo,
32
+ s?: string,
33
+ pkgType?: PackageType | null
34
+ ) => syncService.handlePurchase(u, p, c, s as any, pkgType),
35
+ onRenewalDetected: (
36
+ u: string,
37
+ p: string,
38
+ expires: string,
39
+ c: CustomerInfo
40
+ ) => syncService.handleRenewal(u, p, expires, c),
41
+ onPremiumStatusChanged: (
42
+ u: string,
43
+ isP: boolean,
44
+ pId?: string,
45
+ exp?: string,
46
+ willR?: boolean,
47
+ pt?: string
48
+ ) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt as any),
29
49
  onCreditsUpdated,
30
50
  },
31
51
  apiKey,
@@ -6,8 +6,6 @@ import { emitCreditsUpdated } from "./syncEventEmitter";
6
6
  import { generateInitSyncId, generateStatusSyncId } from "./syncIdGenerators";
7
7
  import { NO_SUBSCRIPTION_PRODUCT_ID, DEFAULT_FREE_USER_DATA } from "./syncConstants";
8
8
 
9
- declare const __DEV__: boolean;
10
-
11
9
  export const handleExpiredSubscription = async (userId: string): Promise<void> => {
12
10
  await getCreditsRepository().syncExpiredStatus(userId);
13
11
  emitCreditsUpdated(userId);
@@ -34,16 +32,6 @@ export const handlePremiumStatusSync = async (
34
32
  willRenew: boolean,
35
33
  periodType: PeriodType | null
36
34
  ): Promise<void> => {
37
- if (typeof __DEV__ !== "undefined" && __DEV__) {
38
- console.log("[StatusChangeHandlers] handlePremiumStatusSync called:", {
39
- userId,
40
- isPremium,
41
- productId,
42
- expiresAt,
43
- willRenew,
44
- periodType,
45
- });
46
- }
47
35
 
48
36
  const revenueCatData: RevenueCatData = {
49
37
  expirationDate: expiresAt,
@@ -60,16 +48,6 @@ export const handlePremiumStatusSync = async (
60
48
 
61
49
  const statusSyncId = generateStatusSyncId(userId, isPremium);
62
50
 
63
- if (typeof __DEV__ !== "undefined" && __DEV__) {
64
- console.log("[StatusChangeHandlers] Calling initializeCredits with:", {
65
- userId,
66
- statusSyncId,
67
- productId,
68
- source: PURCHASE_SOURCE.SETTINGS,
69
- type: PURCHASE_TYPE.INITIAL,
70
- });
71
- }
72
-
73
51
  await getCreditsRepository().initializeCredits(
74
52
  userId,
75
53
  statusSyncId,
@@ -79,13 +57,5 @@ export const handlePremiumStatusSync = async (
79
57
  PURCHASE_TYPE.INITIAL
80
58
  );
81
59
 
82
- if (typeof __DEV__ !== "undefined" && __DEV__) {
83
- console.log("[StatusChangeHandlers] initializeCredits completed, emitting credits updated event");
84
- }
85
-
86
60
  emitCreditsUpdated(userId);
87
-
88
- if (typeof __DEV__ !== "undefined" && __DEV__) {
89
- console.log("[StatusChangeHandlers] ✅ handlePremiumStatusSync completed successfully");
90
- }
91
61
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Subscription Threshold Constants
3
+ * Centralized threshold values for subscription UI logic
4
+ */
5
+
6
+ /**
7
+ * Number of days before expiration to show warnings
8
+ * Used across subscription UI components for consistency
9
+ */
10
+ export const EXPIRATION_WARNING_THRESHOLD_DAYS = 7;
@@ -32,15 +32,15 @@ export class PurchaseStatusResolver {
32
32
  expirationDate: toDate(entitlement.expirationDate),
33
33
  willRenew: entitlement.willRenew ?? false,
34
34
  productIdentifier,
35
- originalPurchaseDate: toDate(entitlement.originalPurchaseDate) ?? null,
36
- latestPurchaseDate: toDate(entitlement.latestPurchaseDate) ?? null,
35
+ originalPurchaseDate: toDate(entitlement.originalPurchaseDate),
36
+ latestPurchaseDate: toDate(entitlement.latestPurchaseDate),
37
37
  billingIssuesDetected: entitlement.billingIssueDetectedAt !== null && entitlement.billingIssueDetectedAt !== undefined,
38
38
  isSandbox: entitlement.isSandbox ?? false,
39
39
  periodType: entitlement.periodType ?? null,
40
40
  packageType: detectedPackageType,
41
41
  store: null,
42
42
  gracePeriodExpiresDate: null,
43
- unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt) ?? null,
43
+ unsubscribeDetectedAt: toDate(entitlement.unsubscribeDetectedAt),
44
44
  };
45
45
  }
46
46
 
@@ -1,8 +1,6 @@
1
1
  import type { PurchasesPackage } from "react-native-purchases";
2
2
  import type { IRevenueCatService } from "../../../../../shared/application/ports/IRevenueCatService";
3
3
 
4
- declare const __DEV__: boolean;
5
-
6
4
  export async function fetchPackages(
7
5
  service: IRevenueCatService
8
6
  ): Promise<PurchasesPackage[]> {
@@ -13,26 +11,12 @@ export async function fetchPackages(
13
11
  try {
14
12
  const offering = await service.fetchOfferings();
15
13
 
16
- if (__DEV__) {
17
- console.log('[PackageHandler] fetchOfferings result:', {
18
- hasOffering: !!offering,
19
- offeringId: offering?.identifier,
20
- packagesCount: offering?.availablePackages?.length,
21
- });
22
- }
23
-
24
14
  if (!offering) {
25
- if (__DEV__) {
26
- console.warn('[PackageHandler] No offering returned, returning empty array');
27
- }
28
15
  return [];
29
16
  }
30
17
 
31
18
  const packages = offering.availablePackages;
32
19
  if (!packages || packages.length === 0) {
33
- if (__DEV__) {
34
- console.warn('[PackageHandler] Offering has no packages, returning empty array');
35
- }
36
20
  return [];
37
21
  }
38
22
 
@@ -45,9 +29,6 @@ export async function fetchPackages(
45
29
 
46
30
  return packages;
47
31
  } catch (error) {
48
- if (__DEV__) {
49
- console.error('[PackageHandler] Error fetching packages:', error);
50
- }
51
32
  throw new Error(
52
33
  `Failed to fetch subscription packages. ${
53
34
  error instanceof Error ? error.message : "Unknown error"
@@ -45,7 +45,7 @@ export function useCustomerInfo(): UseCustomerInfoResult {
45
45
  listenerRef.current = null;
46
46
  }
47
47
  };
48
- }, [fetchCustomerInfo]);
48
+ }, [fetchCustomerInfo]); // fetchCustomerInfo is stable (empty deps), included for lint
49
49
 
50
50
  return {
51
51
  customerInfo,
@@ -6,6 +6,7 @@
6
6
  import { useMutation, useQueryClient } from "@umituz/react-native-design-system";
7
7
  import { SubscriptionManager } from '../../infrastructure/managers/SubscriptionManager';
8
8
  import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
9
+ import { requireAuthentication } from "../../utils/authGuards";
9
10
 
10
11
  /**
11
12
  * Initialize subscription with RevenueCat
@@ -15,10 +16,7 @@ export const useInitializeSubscription = (userId: string | undefined) => {
15
16
 
16
17
  return useMutation({
17
18
  mutationFn: async () => {
18
- if (!userId) {
19
- throw new Error("User not authenticated");
20
- }
21
-
19
+ requireAuthentication(userId);
22
20
  return SubscriptionManager.initialize(userId);
23
21
  },
24
22
  onSuccess: () => {
@@ -29,17 +29,27 @@ export const usePaywallFlow = (options: UsePaywallFlowOptions = {}): UsePaywallF
29
29
 
30
30
  // Load persisted state
31
31
  useEffect(() => {
32
+ let isMounted = true;
33
+
32
34
  const loadPersistedState = async () => {
33
35
  try {
34
36
  const value = await getString(PAYWALL_SHOWN_KEY, '');
35
- setPaywallShown(value === 'true');
37
+ if (isMounted) {
38
+ setPaywallShown(value === 'true');
39
+ }
36
40
  } catch (error) {
37
41
  console.error('[usePaywallFlow] Failed to load paywall state', error);
38
- setPaywallShown(false); // Safe default
42
+ if (isMounted) {
43
+ setPaywallShown(false); // Safe default
44
+ }
39
45
  }
40
46
  };
41
47
 
42
48
  loadPersistedState();
49
+
50
+ return () => {
51
+ isMounted = false;
52
+ };
43
53
  }, [getString]);
44
54
 
45
55
  const closePostOnboardingPaywall = useCallback(async (_isPremium: boolean) => {