@umituz/react-native-subscription 2.17.6 → 2.17.8

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