@umituz/react-native-subscription 2.33.9 → 2.35.0

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/DeductCreditsCommand.ts +11 -0
  3. package/src/domains/credits/application/creditOperationUtils.ts +5 -1
  4. package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +6 -1
  5. package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +11 -4
  6. package/src/domains/credits/presentation/useCredits.ts +2 -7
  7. package/src/domains/paywall/hooks/usePaywallActions.ts +50 -16
  8. package/src/domains/revenuecat/infrastructure/services/ConfigurationStateManager.ts +16 -4
  9. package/src/domains/revenuecat/infrastructure/services/RevenueCatInitializer.ts +17 -4
  10. package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +63 -4
  11. package/src/domains/subscription/application/SubscriptionAuthListener.ts +45 -8
  12. package/src/domains/subscription/application/SubscriptionSyncService.ts +24 -6
  13. package/src/domains/subscription/application/SubscriptionSyncUtils.ts +32 -9
  14. package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +30 -3
  15. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +7 -4
  16. package/src/domains/subscription/application/initializer/SubscriptionInitializer.ts +3 -2
  17. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +9 -1
  18. package/src/domains/subscription/infrastructure/handlers/package-operations/PackagePurchaser.ts +10 -0
  19. package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +6 -0
  20. package/src/domains/subscription/infrastructure/managers/SubscriptionInternalState.ts +1 -4
  21. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +36 -11
  22. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.types.ts +0 -1
  23. package/src/domains/subscription/infrastructure/managers/managerOperations.ts +10 -1
  24. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
  25. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +13 -5
  26. package/src/domains/subscription/infrastructure/services/PurchaseHandler.ts +17 -1
  27. package/src/domains/subscription/infrastructure/services/RevenueCatService.types.ts +10 -1
  28. package/src/domains/subscription/infrastructure/services/purchase/PurchaseExecutor.ts +23 -4
  29. package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +2 -7
  30. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +7 -3
  31. package/src/domains/subscription/infrastructure/utils/renewal/RenewalDetector.ts +2 -2
  32. package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +4 -4
  33. package/src/init/createSubscriptionInitModule.ts +1 -1
  34. package/src/shared/infrastructure/SubscriptionEventBus.ts +10 -5
  35. package/src/shared/presentation/hooks/useServiceCall.ts +15 -5
  36. package/src/domains/subscription/infrastructure/utils/UserIdProvider.ts +0 -30
@@ -5,11 +5,15 @@ import { SubscriptionSyncService } from "../SubscriptionSyncService";
5
5
  import type { SubscriptionInitConfig } from "../SubscriptionInitializerTypes";
6
6
 
7
7
  export function configureServices(config: SubscriptionInitConfig, apiKey: string): SubscriptionSyncService {
8
- const { entitlementId, credits, creditPackages, getAnonymousUserId, getFirebaseAuth, showAuthModal, onCreditsUpdated } = config;
8
+ const { entitlementId, credits, creditPackages, getFirebaseAuth, showAuthModal, onCreditsUpdated } = config;
9
+
10
+ if (!creditPackages) {
11
+ throw new Error('[ServiceConfigurator] creditPackages configuration is required');
12
+ }
9
13
 
10
14
  configureCreditsRepository({
11
15
  ...credits,
12
- creditPackageAmounts: creditPackages!.amounts
16
+ creditPackageAmounts: creditPackages.amounts
13
17
  });
14
18
 
15
19
  const syncService = new SubscriptionSyncService(entitlementId);
@@ -18,14 +22,13 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
18
22
  config: {
19
23
  apiKey,
20
24
  entitlementIdentifier: entitlementId,
21
- consumableProductIdentifiers: [creditPackages!.identifierPattern],
25
+ consumableProductIdentifiers: [creditPackages.identifierPattern],
22
26
  onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
23
27
  onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
24
28
  onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
25
29
  onCreditsUpdated,
26
30
  },
27
31
  apiKey,
28
- getAnonymousUserId: getAnonymousUserId!,
29
32
  });
30
33
 
31
34
  configureAuthProvider({
@@ -3,9 +3,10 @@ import { getApiKey, validateConfig } from "./ConfigValidator";
3
3
  import { configureServices } from "./ServiceConfigurator";
4
4
  import { startBackgroundInitialization } from "./BackgroundInitializer";
5
5
 
6
- export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
6
+ export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<() => void> => {
7
7
  const apiKey = getApiKey(config);
8
8
  validateConfig(config);
9
9
  configureServices(config, apiKey);
10
- await startBackgroundInitialization(config);
10
+ const cleanup = await startBackgroundInitialization(config);
11
+ return cleanup;
11
12
  };
@@ -24,7 +24,15 @@ export class PackageHandler {
24
24
  }
25
25
 
26
26
  async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
27
- return executePurchase(this.service, pkg, userId);
27
+ console.log('🔵 [PackageHandler] purchase called', {
28
+ productId: pkg.product.identifier,
29
+ userId
30
+ });
31
+
32
+ const result = await executePurchase(this.service, pkg, userId);
33
+ console.log('✅ [PackageHandler] purchase completed', { result });
34
+
35
+ return result;
28
36
  }
29
37
 
30
38
  async restore(userId: string): Promise<RestoreResultInfo> {
@@ -6,10 +6,20 @@ export async function executePurchase(
6
6
  pkg: PurchasesPackage,
7
7
  userId: string
8
8
  ): Promise<boolean> {
9
+ console.log('🔵 [executePurchase] Starting', {
10
+ productId: pkg.product.identifier,
11
+ userId,
12
+ isInitialized: service.isInitialized()
13
+ });
14
+
9
15
  if (!service.isInitialized()) {
16
+ console.error('❌ [executePurchase] Service not initialized!');
10
17
  throw new Error("Service not initialized");
11
18
  }
12
19
 
20
+ console.log('🚀 [executePurchase] Calling service.purchasePackage');
13
21
  const result = await service.purchasePackage(pkg, userId);
22
+ console.log('✅ [executePurchase] Completed', { success: result.success });
23
+
14
24
  return result.success;
15
25
  }
@@ -55,10 +55,16 @@ export const useSubscriptionPackages = () => {
55
55
 
56
56
  if (prevUserId !== userId) {
57
57
  if (prevUserId) {
58
+ queryClient.cancelQueries({
59
+ queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
60
+ });
58
61
  queryClient.removeQueries({
59
62
  queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, prevUserId],
60
63
  });
61
64
  } else {
65
+ queryClient.cancelQueries({
66
+ queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
67
+ });
62
68
  queryClient.removeQueries({
63
69
  queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, "anonymous"],
64
70
  });
@@ -1,12 +1,9 @@
1
- import { UserIdProvider } from "../utils/UserIdProvider";
2
1
  import { InitializationCache } from "../utils/InitializationCache";
3
2
 
4
3
  export class SubscriptionInternalState {
5
- public userIdProvider = new UserIdProvider();
6
4
  public initCache = new InitializationCache();
7
-
5
+
8
6
  reset() {
9
- this.userIdProvider.reset();
10
7
  this.initCache.reset();
11
8
  }
12
9
  }
@@ -17,7 +17,6 @@ class SubscriptionManagerImpl {
17
17
 
18
18
  configure(config: SubscriptionManagerConfig): void {
19
19
  this.managerConfig = config;
20
- this.state.userIdProvider.configure(config.getAnonymousUserId);
21
20
  }
22
21
 
23
22
  private ensureConfigured(): void {
@@ -34,19 +33,22 @@ class SubscriptionManagerImpl {
34
33
  async initialize(userId?: string): Promise<boolean> {
35
34
  this.ensureConfigured();
36
35
 
37
- let actualUserId: string | null = null;
36
+ const actualUserId: string = (userId && userId.length > 0) ? userId : '';
38
37
 
39
- if (userId && userId.length > 0) {
40
- actualUserId = userId;
41
- } else {
42
- const anonymousId = await this.managerConfig.getAnonymousUserId();
43
- actualUserId = (anonymousId && anonymousId.length > 0) ? anonymousId : null;
38
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
39
+ console.log('[SubscriptionManager] initialize called:', {
40
+ providedUserId: userId,
41
+ actualUserId: actualUserId || '(empty - RevenueCat will generate anonymous ID)',
42
+ });
44
43
  }
45
44
 
46
- const cacheKey = actualUserId ?? '__anonymous__';
45
+ const cacheKey = actualUserId || '__anonymous__';
47
46
  const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(cacheKey);
48
47
 
49
48
  if (!shouldInit && existingPromise) {
49
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
50
+ console.log('[SubscriptionManager] Using cached initialization for:', cacheKey);
51
+ }
50
52
  return existingPromise;
51
53
  }
52
54
 
@@ -55,11 +57,23 @@ class SubscriptionManagerImpl {
55
57
  return promise;
56
58
  }
57
59
 
58
- private async performInitialization(userId: string | null): Promise<boolean> {
60
+ private async performInitialization(userId: string): Promise<boolean> {
59
61
  this.ensureConfigured();
60
- const { service, success } = await performServiceInitialization(this.managerConfig.config, userId ?? '');
62
+
63
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
64
+ console.log('[SubscriptionManager] performInitialization:', {
65
+ userId: userId || '(empty - anonymous)',
66
+ });
67
+ }
68
+
69
+ const { service, success } = await performServiceInitialization(this.managerConfig.config, userId);
61
70
  this.serviceInstance = service ?? null;
62
71
  this.ensurePackageHandlerInitialized();
72
+
73
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
74
+ console.log('[SubscriptionManager] Initialization completed:', { success });
75
+ }
76
+
63
77
  return success;
64
78
  }
65
79
 
@@ -73,9 +87,20 @@ class SubscriptionManagerImpl {
73
87
  }
74
88
 
75
89
  async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
90
+ console.log('🔵 [SubscriptionManager] purchasePackage called', {
91
+ productId: pkg.product.identifier,
92
+ isConfigured: !!this.managerConfig,
93
+ hasPackageHandler: !!this.packageHandler
94
+ });
95
+
76
96
  this.ensureConfigured();
77
97
  this.ensurePackageHandlerInitialized();
78
- return purchasePackageOperation(pkg, this.managerConfig, this.state, this.packageHandler!);
98
+
99
+ console.log('🚀 [SubscriptionManager] Calling purchasePackageOperation');
100
+ const result = await purchasePackageOperation(pkg, this.managerConfig, this.state, this.packageHandler!);
101
+ console.log('✅ [SubscriptionManager] purchasePackageOperation completed', { result });
102
+
103
+ return result;
79
104
  }
80
105
 
81
106
  async restore(): Promise<RestoreResultInfo> {
@@ -4,7 +4,6 @@ import type { PremiumStatus } from "../handlers/PurchaseStatusResolver";
4
4
  export interface SubscriptionManagerConfig {
5
5
  config: RevenueCatConfig;
6
6
  apiKey: string;
7
- getAnonymousUserId: () => Promise<string>;
8
7
  }
9
8
 
10
9
  export type { PremiumStatus };
@@ -22,9 +22,18 @@ export const purchasePackageOperation = async (
22
22
  state: SubscriptionInternalState,
23
23
  packageHandler: PackageHandler
24
24
  ): Promise<boolean> => {
25
+ console.log('🔵 [purchasePackageOperation] Starting', {
26
+ productId: pkg.product.identifier
27
+ });
28
+
25
29
  ensureConfigured(managerConfig);
26
30
  const userId = getCurrentUserIdOrThrow(state);
27
- return packageHandler.purchase(pkg, userId);
31
+
32
+ console.log('🚀 [purchasePackageOperation] Calling packageHandler.purchase', { userId });
33
+ const result = await packageHandler.purchase(pkg, userId);
34
+ console.log('✅ [purchasePackageOperation] Completed', { result });
35
+
36
+ return result;
28
37
  };
29
38
 
30
39
  export const restoreOperation = async (
@@ -22,8 +22,8 @@ export function ensureConfigured(config: SubscriptionManagerConfig | null): void
22
22
  */
23
23
  export function getCurrentUserIdOrThrow(state: SubscriptionInternalState): string {
24
24
  const userId = state.initCache.getCurrentUserId();
25
- if (!userId) {
26
- throw new Error("No current user found");
25
+ if (userId === null || userId === undefined) {
26
+ throw new Error("SubscriptionManager not initialized - no current user ID available");
27
27
  }
28
28
  return userId;
29
29
  }
@@ -55,22 +55,30 @@ export class CustomerInfoListenerManager {
55
55
  });
56
56
  }
57
57
 
58
- if (!this.state.currentUserId) {
58
+ const capturedUserId = this.state.currentUserId;
59
+ if (!capturedUserId) {
59
60
  if (typeof __DEV__ !== "undefined" && __DEV__) {
60
61
  console.log("[CustomerInfoListener] No userId - skipping");
61
62
  }
62
63
  return;
63
64
  }
64
65
 
65
- this.state.renewalState = await processCustomerInfo(
66
+ const newRenewalState = await processCustomerInfo(
66
67
  customerInfo,
67
- this.state.currentUserId,
68
+ capturedUserId,
68
69
  this.state.renewalState,
69
70
  config
70
71
  );
71
72
 
72
- if (typeof __DEV__ !== "undefined" && __DEV__) {
73
- console.log("[CustomerInfoListener] processCustomerInfo completed");
73
+ if (this.state.currentUserId === capturedUserId) {
74
+ this.state.renewalState = newRenewalState;
75
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
76
+ console.log("[CustomerInfoListener] processCustomerInfo completed");
77
+ }
78
+ } else {
79
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
80
+ console.log("[CustomerInfoListener] User changed during processing - discarding result");
81
+ }
74
82
  }
75
83
  };
76
84
 
@@ -16,22 +16,38 @@ export async function handlePurchase(
16
16
  pkg: PurchasesPackage,
17
17
  userId: string
18
18
  ): Promise<PurchaseResult> {
19
+ console.log('🔵 [PurchaseHandler] handlePurchase called', {
20
+ productId: pkg.product.identifier,
21
+ userId,
22
+ isInitialized: deps.isInitialized()
23
+ });
24
+
19
25
  validatePurchaseReady(deps.isInitialized());
20
26
 
21
27
  const consumableIds = deps.config.consumableProductIdentifiers || [];
22
28
  const isConsumable = isConsumableProduct(pkg, consumableIds);
23
29
 
30
+ console.log('📦 [PurchaseHandler] Product type', { isConsumable });
31
+
24
32
  try {
25
- return await executePurchase(deps.config, userId, pkg, isConsumable);
33
+ console.log('🚀 [PurchaseHandler] Calling executePurchase');
34
+ const result = await executePurchase(deps.config, userId, pkg, isConsumable);
35
+ console.log('✅ [PurchaseHandler] executePurchase completed', { success: result.success });
36
+ return result;
26
37
  } catch (error) {
38
+ console.error('❌ [PurchaseHandler] Purchase failed', { error });
39
+
27
40
  if (isUserCancelledError(error)) {
41
+ console.log('⚠️ [PurchaseHandler] User cancelled');
28
42
  return { success: false, isPremium: false, productId: pkg.product.identifier };
29
43
  }
30
44
 
31
45
  if (isAlreadyPurchasedError(error)) {
46
+ console.log('⚠️ [PurchaseHandler] Already purchased');
32
47
  return await handleAlreadyPurchasedError(deps, userId, pkg, error);
33
48
  }
34
49
 
50
+ console.error('❌ [PurchaseHandler] Unhandled error');
35
51
  return handlePurchaseError(error, pkg, userId);
36
52
  }
37
53
  }
@@ -60,7 +60,16 @@ export class RevenueCatService implements IRevenueCatService {
60
60
  }
61
61
 
62
62
  async purchasePackage(pkg: PurchasesPackage, userId: string): Promise<PurchaseResult> {
63
- return handlePurchase(this.getSDKParams(), pkg, userId);
63
+ console.log('🔵 [RevenueCatService] purchasePackage called', {
64
+ productId: pkg.product.identifier,
65
+ userId,
66
+ isInitialized: this.isInitialized()
67
+ });
68
+
69
+ const result = await handlePurchase(this.getSDKParams(), pkg, userId);
70
+ console.log('✅ [RevenueCatService] purchasePackage completed', { success: result.success });
71
+
72
+ return result;
64
73
  }
65
74
 
66
75
  async restorePurchases(userId: string): Promise<RestoreResult> {
@@ -10,9 +10,14 @@ async function executeConsumablePurchase(
10
10
  productId: string,
11
11
  customerInfo: CustomerInfo
12
12
  ): Promise<PurchaseResult> {
13
- const source = getSavedPurchase()?.source;
13
+ const savedPurchase = getSavedPurchase();
14
+ const source = savedPurchase?.source;
15
+ if (savedPurchase) {
16
+ clearSavedPurchase();
17
+ }
18
+
14
19
  await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
15
- clearSavedPurchase();
20
+
16
21
  return {
17
22
  success: true,
18
23
  isPremium: false,
@@ -32,7 +37,11 @@ async function executeSubscriptionPurchase(
32
37
  entitlementIdentifier: string
33
38
  ): Promise<PurchaseResult> {
34
39
  const isPremium = !!customerInfo.entitlements.active[entitlementIdentifier];
35
- const source = getSavedPurchase()?.source;
40
+ const savedPurchase = getSavedPurchase();
41
+ const source = savedPurchase?.source;
42
+ if (savedPurchase) {
43
+ clearSavedPurchase();
44
+ }
36
45
 
37
46
  if (typeof __DEV__ !== "undefined" && __DEV__) {
38
47
  console.log("[PurchaseExecutor] executeSubscriptionPurchase:", {
@@ -52,7 +61,6 @@ async function executeSubscriptionPurchase(
52
61
  }
53
62
 
54
63
  await notifyPurchaseCompleted(config, userId, productId, customerInfo, source);
55
- clearSavedPurchase();
56
64
 
57
65
  if (typeof __DEV__ !== "undefined" && __DEV__) {
58
66
  console.log("[PurchaseExecutor] Purchase flow completed successfully");
@@ -72,13 +80,24 @@ export async function executePurchase(
72
80
  pkg: PurchasesPackage,
73
81
  isConsumable: boolean
74
82
  ): Promise<PurchaseResult> {
83
+ console.log('🔵 [PurchaseExecutor] executePurchase called', {
84
+ productId: pkg.product.identifier,
85
+ userId,
86
+ isConsumable
87
+ });
88
+
89
+ console.log('🚀 [PurchaseExecutor] Calling Purchases.purchasePackage (RevenueCat SDK)');
75
90
  const { customerInfo } = await Purchases.purchasePackage(pkg);
91
+ console.log('✅ [PurchaseExecutor] Purchases.purchasePackage completed');
92
+
76
93
  const productId = pkg.product.identifier;
77
94
 
78
95
  if (isConsumable) {
96
+ console.log('💰 [PurchaseExecutor] Processing as consumable purchase');
79
97
  return executeConsumablePurchase(config, userId, productId, customerInfo);
80
98
  }
81
99
 
100
+ console.log('📅 [PurchaseExecutor] Processing as subscription purchase');
82
101
  return executeSubscriptionPurchase(
83
102
  config,
84
103
  userId,
@@ -48,10 +48,8 @@ export class InitializationCache {
48
48
  this.initPromise = promise;
49
49
  this.promiseUserId = userId;
50
50
 
51
- // Capture userId to prevent stale reference after catch clears promiseUserId
52
51
  const targetUserId = userId;
53
52
 
54
- // Chain to mark completion and set currentUserId only on success
55
53
  promise
56
54
  .then((result) => {
57
55
  if (result && this.promiseUserId === targetUserId) {
@@ -61,7 +59,6 @@ export class InitializationCache {
61
59
  return result;
62
60
  })
63
61
  .catch((error) => {
64
- // On failure, clear the promise so retry is possible
65
62
  if (this.promiseUserId === targetUserId) {
66
63
  this.initPromise = null;
67
64
  this.promiseUserId = null;
@@ -69,12 +66,10 @@ export class InitializationCache {
69
66
  }
70
67
  this.promiseCompleted = true;
71
68
  console.error('[InitializationCache] Initialization failed', { userId: targetUserId, error });
72
- // Re-throw so callers awaiting the promise see the error
73
- throw error;
69
+ return false;
74
70
  })
75
71
  .finally(() => {
76
- // Always release the mutex
77
- if (this.promiseUserId === targetUserId) {
72
+ if (this.initializationInProgress) {
78
73
  this.initializationInProgress = false;
79
74
  }
80
75
  });
@@ -14,7 +14,7 @@ export async function syncPremiumStatus(
14
14
  config: RevenueCatConfig,
15
15
  userId: string,
16
16
  customerInfo: CustomerInfo
17
- ): Promise<void> {
17
+ ): Promise<{ success: boolean; error?: Error }> {
18
18
  if (typeof __DEV__ !== "undefined" && __DEV__) {
19
19
  console.log("[PremiumStatusSyncer] syncPremiumStatus called:", {
20
20
  userId,
@@ -28,7 +28,7 @@ export async function syncPremiumStatus(
28
28
  if (typeof __DEV__ !== "undefined" && __DEV__) {
29
29
  console.log("[PremiumStatusSyncer] No onPremiumStatusChanged callback - skipping");
30
30
  }
31
- return;
31
+ return { success: true };
32
32
  }
33
33
 
34
34
  const premiumEntitlement = getPremiumEntitlement(
@@ -68,6 +68,7 @@ export async function syncPremiumStatus(
68
68
  if (typeof __DEV__ !== "undefined" && __DEV__) {
69
69
  console.log("[PremiumStatusSyncer] onPremiumStatusChanged completed successfully");
70
70
  }
71
+ return { success: true };
71
72
  } catch (error) {
72
73
  console.error('[PremiumStatusSyncer] Premium status change callback failed', {
73
74
  userId,
@@ -75,7 +76,10 @@ export async function syncPremiumStatus(
75
76
  productId: premiumEntitlement?.productIdentifier,
76
77
  error
77
78
  });
78
- // Silently fail callback notifications to prevent crashing the main flow
79
+ return {
80
+ success: false,
81
+ error: error instanceof Error ? error : new Error(String(error))
82
+ };
79
83
  }
80
84
  }
81
85
 
@@ -42,8 +42,8 @@ export function detectRenewal(
42
42
  };
43
43
  }
44
44
 
45
- const newExpiration = new Date(newExpirationDate);
46
- const previousExpiration = new Date(state.previousExpirationDate);
45
+ const newExpiration = new Date(newExpirationDate).getTime();
46
+ const previousExpiration = new Date(state.previousExpirationDate).getTime();
47
47
  const productChanged = productId !== state.previousProductId;
48
48
  const expirationExtended = newExpiration > previousExpiration;
49
49
 
@@ -25,12 +25,12 @@ export interface PurchaseLoadingActions {
25
25
 
26
26
  export type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
27
27
 
28
- const initialState: PurchaseLoadingState = {
28
+ const createInitialState = (): PurchaseLoadingState => ({
29
29
  activePurchases: new Map(),
30
- };
30
+ });
31
31
 
32
32
  export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) => ({
33
- ...initialState,
33
+ ...createInitialState(),
34
34
 
35
35
  startPurchase: (productId, source) => {
36
36
  set((state) => {
@@ -55,7 +55,7 @@ export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set, get) =
55
55
  },
56
56
 
57
57
  reset: () => {
58
- set(initialState);
58
+ set(createInitialState());
59
59
  },
60
60
  }));
61
61
 
@@ -21,7 +21,7 @@ export function createSubscriptionInitModule(config: SubscriptionInitModuleConfi
21
21
  return true;
22
22
  }
23
23
 
24
- await initializeSubscription({ apiKey, ...subscriptionConfig });
24
+ const cleanup = await initializeSubscription({ apiKey, ...subscriptionConfig });
25
25
  return true;
26
26
  } catch {
27
27
  return false;
@@ -39,12 +39,17 @@ export class SubscriptionEventBus {
39
39
 
40
40
  emit<T>(event: string, data: T): void {
41
41
  if (!this.listeners[event]) return;
42
+
42
43
  this.listeners[event].forEach(callback => {
43
- try {
44
- callback(data);
45
- } catch (error) {
46
- console.error('[SubscriptionEventBus] Listener error for event:', event, { error });
47
- }
44
+ Promise.resolve().then(() => {
45
+ try {
46
+ callback(data);
47
+ } catch (error) {
48
+ console.error('[SubscriptionEventBus] Listener error for event:', event, { error });
49
+ }
50
+ }).catch(error => {
51
+ console.error('[SubscriptionEventBus] Async listener error for event:', event, { error });
52
+ });
48
53
  });
49
54
  }
50
55
 
@@ -3,7 +3,7 @@
3
3
  * Shared hook for handling service calls with loading, error, and success states
4
4
  */
5
5
 
6
- import { useState, useCallback } from "react";
6
+ import { useState, useCallback, useRef, useEffect } from "react";
7
7
 
8
8
  export interface ServiceCallState<T> {
9
9
  data: T | null;
@@ -36,21 +36,31 @@ export function useServiceCall<T>(
36
36
  error: null,
37
37
  });
38
38
 
39
+ const onSuccessRef = useRef(onSuccess);
40
+ const onErrorRef = useRef(onError);
41
+ const onCompleteRef = useRef(onComplete);
42
+
43
+ useEffect(() => {
44
+ onSuccessRef.current = onSuccess;
45
+ onErrorRef.current = onError;
46
+ onCompleteRef.current = onComplete;
47
+ });
48
+
39
49
  const execute = useCallback(async () => {
40
50
  setState({ data: null, isLoading: true, error: null });
41
51
 
42
52
  try {
43
53
  const data = await serviceFn();
44
54
  setState({ data, isLoading: false, error: null });
45
- onSuccess?.(data);
55
+ onSuccessRef.current?.(data);
46
56
  } catch (error) {
47
57
  const errorObj = error instanceof Error ? error : new Error("Service call failed");
48
58
  setState({ data: null, isLoading: false, error: errorObj });
49
- onError?.(errorObj);
59
+ onErrorRef.current?.(errorObj);
50
60
  } finally {
51
- onComplete?.();
61
+ onCompleteRef.current?.();
52
62
  }
53
- }, [serviceFn, onSuccess, onError, onComplete]);
63
+ }, [serviceFn]);
54
64
 
55
65
  const reset = useCallback(() => {
56
66
  setState({ data: null, isLoading: false, error: null });
@@ -1,30 +0,0 @@
1
- /**
2
- * User ID Provider
3
- * Manages user ID retrieval (anonymous or authenticated)
4
- */
5
-
6
- export class UserIdProvider {
7
- private cachedAnonUserId: string | null = null;
8
- private getAnonymousUserIdFn: (() => Promise<string>) | null = null;
9
-
10
- configure(getAnonymousUserId: () => Promise<string>): void {
11
- this.getAnonymousUserIdFn = getAnonymousUserId;
12
- }
13
-
14
- async getOrCreateAnonymousUserId(): Promise<string> {
15
- if (this.cachedAnonUserId) {
16
- return this.cachedAnonUserId;
17
- }
18
-
19
- if (!this.getAnonymousUserIdFn) {
20
- throw new Error("Anonymous user ID provider not configured");
21
- }
22
-
23
- this.cachedAnonUserId = await this.getAnonymousUserIdFn();
24
- return this.cachedAnonUserId;
25
- }
26
-
27
- reset(): void {
28
- this.cachedAnonUserId = null;
29
- }
30
- }