@umituz/react-native-subscription 2.43.0 → 2.43.2

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditsInitializer.ts +3 -2
  3. package/src/domains/credits/domain/services/CreditLimitService.ts +76 -0
  4. package/src/domains/credits/infrastructure/CreditsRepository.ts +10 -3
  5. package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +3 -2
  6. package/src/domains/paywall/components/PaywallScreen.tsx +8 -8
  7. package/src/domains/paywall/components/PaywallScreen.types.ts +15 -4
  8. package/src/domains/paywall/hooks/usePaywallActions.ts +9 -16
  9. package/src/domains/paywall/hooks/usePaywallOrchestrator.ts +26 -8
  10. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +51 -19
  11. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +9 -3
  12. package/src/domains/subscription/core/events/FlowEvents.ts +24 -0
  13. package/src/domains/subscription/core/events/SubscriptionEvents.ts +22 -0
  14. package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +4 -0
  15. package/src/domains/subscription/presentation/usePremium.ts +24 -87
  16. package/src/domains/subscription/presentation/usePremiumActions.ts +94 -0
  17. package/src/domains/subscription/presentation/usePremiumPackages.ts +25 -0
  18. package/src/domains/subscription/presentation/usePremiumStatus.ts +38 -0
  19. package/src/domains/subscription/presentation/useSubscriptionFlow.ts +7 -3
  20. package/src/domains/subscription/presentation/useSyncStatusListener.ts +47 -0
  21. package/src/index.ts +17 -5
  22. package/src/shared/infrastructure/SubscriptionEventBus.ts +7 -9
  23. package/src/shared/infrastructure/react-query/queryConfig.ts +0 -3
  24. package/src/domains/credits/application/CreditLimitCalculator.ts +0 -10
  25. package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.tsx +0 -60
  26. package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.types.ts +0 -4
  27. package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.43.0",
3
+ "version": "2.43.2",
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",
@@ -5,7 +5,7 @@ import type { InitializeCreditsMetadata, InitializationResult } from "../../subs
5
5
  import { runTransaction, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
6
6
  import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
7
7
  import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
8
- import { calculateCreditLimit } from "./CreditLimitCalculator";
8
+ import { CreditLimitService } from "../domain/services/CreditLimitService";
9
9
  import { generatePurchaseMetadata } from "./PurchaseMetadataGenerator";
10
10
  import { PURCHASE_ID_PREFIXES } from "../core/CreditsConstants";
11
11
 
@@ -69,7 +69,8 @@ export async function initializeCreditsTransaction(
69
69
  });
70
70
  }
71
71
 
72
- const creditLimit = calculateCreditLimit(metadata.productId, config);
72
+ const creditLimitService = new CreditLimitService(config);
73
+ const creditLimit = creditLimitService.calculate(metadata.productId);
73
74
  const { purchaseHistory } = generatePurchaseMetadata({
74
75
  productId: metadata.productId,
75
76
  source: metadata.source,
@@ -0,0 +1,76 @@
1
+ import type { CreditsConfig } from "../../core/Credits";
2
+ import { detectPackageType } from "../../../../utils/packageTypeDetector";
3
+ import { getCreditAllocation } from "../../../../utils/creditMapper";
4
+
5
+ /**
6
+ * Domain service for credit limit calculations.
7
+ *
8
+ * This service contains business logic for determining credit limits
9
+ * based on product configuration. It's part of the domain layer and can be
10
+ * used by infrastructure, application, or presentation layers.
11
+ */
12
+ export class CreditLimitService {
13
+ constructor(private config: CreditsConfig) {}
14
+
15
+ /**
16
+ * Calculate credit limit for a specific product ID.
17
+ *
18
+ * Strategy:
19
+ * 1. Check for explicit amount override in config
20
+ * 2. Calculate from package type allocations
21
+ * 3. Throw if cannot determine limit
22
+ *
23
+ * @param productId - The product identifier
24
+ * @returns The credit limit for this product
25
+ * @throws Error if productId is missing or limit cannot be determined
26
+ */
27
+ calculate(productId: string | undefined): number {
28
+ if (!productId) {
29
+ throw new Error(
30
+ "[CreditLimitService] Cannot calculate credit limit without productId"
31
+ );
32
+ }
33
+
34
+ // Check for explicit amount override
35
+ const explicitAmount = this.config.creditPackageAmounts?.[productId];
36
+ if (
37
+ explicitAmount !== undefined &&
38
+ explicitAmount !== null &&
39
+ typeof explicitAmount === "number"
40
+ ) {
41
+ return explicitAmount;
42
+ }
43
+
44
+ // Calculate from package type allocations
45
+ const packageType = detectPackageType(productId);
46
+ const dynamicLimit = getCreditAllocation(packageType, this.config.packageAllocations);
47
+
48
+ if (dynamicLimit === null || dynamicLimit === undefined) {
49
+ throw new Error(
50
+ `[CreditLimitService] Cannot determine credit limit for productId: ${productId}, packageType: ${packageType}`
51
+ );
52
+ }
53
+
54
+ return dynamicLimit;
55
+ }
56
+
57
+ /**
58
+ * Create a factory function for this service with the given config.
59
+ * This makes dependency injection easier.
60
+ */
61
+ static createFactory(config: CreditsConfig) {
62
+ return () => new CreditLimitService(config);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Convenience function to calculate credit limit without instantiating service.
68
+ * Useful for one-off calculations.
69
+ */
70
+ export function calculateCreditLimit(
71
+ productId: string | undefined,
72
+ config: CreditsConfig
73
+ ): number {
74
+ const service = new CreditLimitService(config);
75
+ return service.calculate(productId);
76
+ }
@@ -11,11 +11,18 @@ import { fetchCredits, checkHasCredits, documentExists } from "./operations/Cred
11
11
  import { syncExpiredStatus, syncPremiumMetadata, createRecoveryCreditsDocument } from "./operations/CreditsWriter";
12
12
  import type { SubscriptionMetadata } from "../../subscription/core/types/SubscriptionMetadata";
13
13
  import { initializeCreditsWithRetry } from "./operations/CreditsInitializer";
14
- import { calculateCreditLimit } from "../application/CreditLimitCalculator";
14
+ import { CreditLimitService } from "../domain/services/CreditLimitService";
15
15
 
16
16
  export class CreditsRepository extends BaseRepository {
17
- constructor(private config: CreditsConfig) {
17
+ private creditLimitService: CreditLimitService;
18
+
19
+ constructor(
20
+ private config: CreditsConfig,
21
+ creditLimitService?: CreditLimitService
22
+ ) {
18
23
  super(config.collectionName);
24
+ // Allow dependency injection or create default instance
25
+ this.creditLimitService = creditLimitService ?? new CreditLimitService(config);
19
26
  }
20
27
 
21
28
  private getCollectionConfig(): CollectionConfig {
@@ -96,7 +103,7 @@ export class CreditsRepository extends BaseRepository {
96
103
  _storeTransactionId?: string | null,
97
104
  ): Promise<boolean> {
98
105
  const db = requireFirestore();
99
- const creditLimit = calculateCreditLimit(productId, this.config);
106
+ const creditLimit = this.creditLimitService.calculate(productId);
100
107
  return createRecoveryCreditsDocument(
101
108
  this.getRef(db, userId),
102
109
  creditLimit,
@@ -4,7 +4,7 @@ import type { PurchaseSource } from "../../core/UserCreditsDocument";
4
4
  import { initializeCreditsTransaction } from "../../application/CreditsInitializer";
5
5
  import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
6
6
  import type { RevenueCatData } from "../../../revenuecat/core/types/RevenueCatData";
7
- import { calculateCreditLimit } from "../../application/CreditLimitCalculator";
7
+ import { CreditLimitService } from "../../domain/services/CreditLimitService";
8
8
  import { PURCHASE_TYPE, type PurchaseType } from "../../../subscription/core/SubscriptionConstants";
9
9
 
10
10
  interface InitializeCreditsParams {
@@ -45,7 +45,8 @@ function isTransientError(error: unknown): boolean {
45
45
  export async function initializeCreditsWithRetry(params: InitializeCreditsParams): Promise<CreditsResult> {
46
46
  const { db, ref, config, userId, purchaseId, productId, source, revenueCatData, type = PURCHASE_TYPE.INITIAL } = params;
47
47
 
48
- const creditLimit = calculateCreditLimit(productId, config);
48
+ const creditLimitService = new CreditLimitService(config);
49
+ const creditLimit = creditLimitService.calculate(productId);
49
50
  const cfg = { ...config, creditLimit };
50
51
 
51
52
  const maxRetries = 3;
@@ -14,7 +14,6 @@ import {
14
14
  StatusBar,
15
15
  } from "react-native";
16
16
  import { useNavigation } from "@react-navigation/native";
17
- import { usePremium } from "../../subscription/presentation/usePremium";
18
17
  import { AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
19
18
  import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
20
19
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -37,13 +36,11 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
37
36
  console.log('[PaywallScreen] 📱 Rendering PaywallScreen', {
38
37
  hasPackages: !!props.packages?.length,
39
38
  packagesCount: props.packages?.length || 0,
39
+ isPremium: props.isPremium,
40
40
  });
41
41
  }
42
42
 
43
- const { purchasePackage, restorePurchase } = usePremium();
44
-
45
43
  const {
46
- onClose,
47
44
  translations,
48
45
  packages = [],
49
46
  features = [],
@@ -52,11 +49,14 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
52
49
  creditAmounts,
53
50
  creditsLabel,
54
51
  heroImage,
52
+ isSyncing,
53
+ onPurchase,
54
+ onRestore,
55
+ onClose,
55
56
  onPurchaseSuccess,
56
57
  onPurchaseError,
57
58
  onAuthRequired,
58
59
  source,
59
- isLoadingPackages
60
60
  } = props;
61
61
 
62
62
  const tokens = useAppDesignTokens();
@@ -80,8 +80,8 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
80
80
  resetState
81
81
  } = usePaywallActions({
82
82
  packages,
83
- purchasePackage,
84
- restorePurchase,
83
+ purchasePackage: onPurchase,
84
+ restorePurchase: onRestore,
85
85
  source,
86
86
  onPurchaseSuccess,
87
87
  onPurchaseError,
@@ -233,7 +233,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
233
233
  return null;
234
234
  }
235
235
 
236
- if (isLoadingPackages) {
236
+ if (isSyncing) {
237
237
  return (
238
238
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary, paddingTop: insets.top }]}>
239
239
  <View style={styles.loadingContainer}>
@@ -2,21 +2,32 @@ import type { ImageSourcePropType } from "react-native";
2
2
  import type { PurchasesPackage } from "react-native-purchases";
3
3
  import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from "../entities/types";
4
4
  import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
5
+ import type { UserCredits } from "../../credits/core/Credits";
5
6
 
6
7
  export interface PaywallScreenProps {
7
- onClose?: () => void;
8
+ // UI Props (required)
8
9
  translations: PaywallTranslations;
9
- packages?: PurchasesPackage[];
10
10
  features?: SubscriptionFeature[];
11
11
  legalUrls?: PaywallLegalUrls;
12
12
  bestValueIdentifier?: string;
13
13
  creditAmounts?: Record<string, number>;
14
14
  creditsLabel?: string;
15
15
  heroImage?: ImageSourcePropType;
16
- // Purchase/restore handlers removed - always use usePremium hook internally
16
+
17
+ // Data Props (required)
18
+ packages: PurchasesPackage[];
19
+ isPremium: boolean;
20
+ credits: UserCredits | null;
21
+ isSyncing: boolean;
22
+
23
+ // Action Props (required)
24
+ onPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
25
+ onRestore: () => Promise<boolean>;
26
+ onClose: () => void;
27
+
28
+ // Optional Callbacks
17
29
  onPurchaseSuccess?: () => void;
18
30
  onPurchaseError?: (error: Error | string) => void;
19
31
  onAuthRequired?: () => void;
20
32
  source?: PurchaseSource;
21
- isLoadingPackages?: boolean;
22
33
  }
@@ -1,6 +1,5 @@
1
1
  import { useState, useCallback, useRef, useMemo } from "react";
2
2
  import type { PurchasesPackage } from "react-native-purchases";
3
- import { usePurchaseLoadingStore, selectIsPurchasing } from "../../subscription/presentation/stores/purchaseLoadingStore";
4
3
  import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
5
4
  import { useSubscriptionStatus } from "../../subscription/presentation/useSubscriptionStatus";
6
5
  import { useCredits } from "../../credits/presentation/useCredits";
@@ -26,17 +25,13 @@ export function usePaywallActions({
26
25
  onClose,
27
26
  }: UsePaywallActionsParams) {
28
27
  const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
29
- const [isLocalProcessing, setIsLocalProcessing] = useState(false);
28
+ const [isProcessing, setIsProcessing] = useState(false);
30
29
 
31
- const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
32
- const { refetch: refetchStatus } = useSubscriptionStatus();
33
- const { refetch: refetchCredits } = useCredits();
34
-
35
- const isProcessing = isLocalProcessing || isGlobalPurchasing;
36
30
  const isProcessingRef = useRef(isProcessing);
37
31
  isProcessingRef.current = isProcessing;
38
32
 
39
- const { startPurchase, endPurchase } = usePurchaseLoadingStore();
33
+ const { refetch: refetchStatus } = useSubscriptionStatus();
34
+ const { refetch: refetchCredits } = useCredits();
40
35
 
41
36
  const purchasePackageRef = useRef(purchasePackage);
42
37
  const restorePurchaseRef = useRef(restorePurchase);
@@ -72,8 +67,7 @@ export function usePaywallActions({
72
67
  });
73
68
  }
74
69
 
75
- setIsLocalProcessing(true);
76
- startPurchase(currentSelectedId, "manual");
70
+ setIsProcessing(true);
77
71
 
78
72
  try {
79
73
  const success = await purchasePackageRef.current(pkg);
@@ -125,10 +119,9 @@ export function usePaywallActions({
125
119
  }
126
120
  onPurchaseErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
127
121
  } finally {
128
- setIsLocalProcessing(false);
129
- endPurchase(currentSelectedId);
122
+ setIsProcessing(false);
130
123
  }
131
- }, [selectedPlanId, startPurchase, endPurchase, refetchStatus, refetchCredits]);
124
+ }, [selectedPlanId, refetchStatus, refetchCredits]);
132
125
 
133
126
  const handleRestore = useCallback(async () => {
134
127
  if (isProcessingRef.current) return;
@@ -137,7 +130,7 @@ export function usePaywallActions({
137
130
  console.log('[usePaywallActions] 🔄 Starting restore');
138
131
  }
139
132
 
140
- setIsLocalProcessing(true);
133
+ setIsProcessing(true);
141
134
  try {
142
135
  const success = await restorePurchaseRef.current();
143
136
 
@@ -172,7 +165,7 @@ export function usePaywallActions({
172
165
  }
173
166
  onPurchaseErrorRef.current?.(error instanceof Error ? error : new Error(String(error)));
174
167
  } finally {
175
- setIsLocalProcessing(false);
168
+ setIsProcessing(false);
176
169
  }
177
170
  }, [refetchStatus, refetchCredits]);
178
171
 
@@ -181,7 +174,7 @@ export function usePaywallActions({
181
174
  console.log('[usePaywallActions] 🧹 Resetting state');
182
175
  }
183
176
  setSelectedPlanId(null);
184
- setIsLocalProcessing(false);
177
+ setIsProcessing(false);
185
178
  }, []);
186
179
 
187
180
  return useMemo(() => ({
@@ -34,12 +34,14 @@ export function usePaywallOrchestrator({
34
34
  bestValueIdentifier = "yearly",
35
35
  creditsLabel,
36
36
  }: PaywallOrchestratorOptions) {
37
- // Sadece isPremium ve packages alıyoruz
38
- // purchasePackage ve restoreRestore fonksiyonlarını ALMIYORUZ
39
- // Çünkü PaywallScreen zaten usePremium hook'unu doğrudan kullanıyor
37
+ // Get all premium data and actions from usePremium
40
38
  const {
41
39
  isPremium,
42
- packages
40
+ packages,
41
+ credits,
42
+ isSyncing,
43
+ purchasePackage,
44
+ restorePurchase,
43
45
  } = usePremium();
44
46
 
45
47
  // Selectors for stable references and fine-grained updates
@@ -55,6 +57,10 @@ export function usePaywallOrchestrator({
55
57
  const { showPaywall, closePaywall } = usePaywallVisibility();
56
58
  const hasNavigatedRef = useRef(false);
57
59
 
60
+ const handleClose = () => {
61
+ closePaywall();
62
+ };
63
+
58
64
  useEffect(() => {
59
65
  if (!isNavReady || !isLocalizationReady) return;
60
66
 
@@ -77,9 +83,8 @@ export function usePaywallOrchestrator({
77
83
  packagesCount: packages.length
78
84
  });
79
85
 
80
- // SADECE DATA geçiyoruz - FONKSİYON YOK
81
- // PaywallScreen kendi usePremium hook'unu kullanacak
82
86
  navigation.navigate("PaywallScreen", {
87
+ // UI Props
83
88
  translations,
84
89
  legalUrls,
85
90
  features,
@@ -87,7 +92,17 @@ export function usePaywallOrchestrator({
87
92
  creditsLabel,
88
93
  heroImage,
89
94
  source: shouldShowPostOnboarding ? "onboarding" : "manual",
95
+
96
+ // Data Props
90
97
  packages,
98
+ isPremium,
99
+ credits,
100
+ isSyncing,
101
+
102
+ // Action Props
103
+ onPurchase: purchasePackage,
104
+ onRestore: restorePurchase,
105
+ onClose: handleClose,
91
106
  });
92
107
 
93
108
  if (shouldShowPostOnboarding) {
@@ -120,13 +135,16 @@ export function usePaywallOrchestrator({
120
135
  closePaywall,
121
136
  bestValueIdentifier,
122
137
  creditsLabel,
138
+ credits,
139
+ isSyncing,
140
+ purchasePackage,
141
+ restorePurchase,
142
+ handleClose,
123
143
  ]);
124
144
 
125
145
  const completeOnboarding = useSubscriptionFlowStore((state) => state.completeOnboarding);
126
146
 
127
147
  return {
128
- isPremium,
129
- packages,
130
148
  flowState: {
131
149
  isOnboardingComplete,
132
150
  showPostOnboardingPaywall,
@@ -1,10 +1,9 @@
1
1
  import { PURCHASE_SOURCE, PURCHASE_TYPE } from "../core/SubscriptionConstants";
2
- import type { PremiumStatusChangedEvent, PurchaseCompletedEvent, RenewalDetectedEvent } from "../core/SubscriptionEvents";
2
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
3
+ import type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "../core/SubscriptionEvents";
3
4
  import { getCreditsRepository } from "../../credits/infrastructure/CreditsRepositoryManager";
4
5
  import { extractRevenueCatData } from "./SubscriptionSyncUtils";
5
6
  import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
6
- import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
7
- import { useSubscriptionFlowStore, SyncStatus } from "../presentation/useSubscriptionFlow";
8
7
 
9
8
  /**
10
9
  * Central processor for all subscription sync operations.
@@ -26,10 +25,14 @@ export class SubscriptionSyncProcessor {
26
25
 
27
26
  // ─── Public API (replaces SubscriptionSyncService) ────────────────
28
27
 
29
- async handlePurchase(event: PurchaseCompletedEvent): Promise<void> {
30
- const store = useSubscriptionFlowStore.getState();
31
- store.setSyncStatus(SyncStatus.SYNCING);
32
-
28
+ async handlePurchase(event: PurchaseCompletedEvent): Promise<{ success: boolean; error?: string }> {
29
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
30
+ status: 'syncing',
31
+ phase: 'purchase',
32
+ userId: event.userId,
33
+ productId: event.productId,
34
+ });
35
+
33
36
  if (typeof __DEV__ !== "undefined" && __DEV__) {
34
37
  console.log('[SubscriptionSyncProcessor] 🔵 PURCHASE START', {
35
38
  userId: event.userId,
@@ -45,7 +48,12 @@ export class SubscriptionSyncProcessor {
45
48
  userId: event.userId,
46
49
  productId: event.productId,
47
50
  });
48
- store.setSyncStatus(SyncStatus.SUCCESS);
51
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
52
+ status: 'success',
53
+ phase: 'purchase',
54
+ userId: event.userId,
55
+ productId: event.productId,
56
+ });
49
57
  if (typeof __DEV__ !== "undefined" && __DEV__) {
50
58
  console.log('[SubscriptionSyncProcessor] 🟢 PURCHASE SUCCESS', {
51
59
  userId: event.userId,
@@ -53,22 +61,33 @@ export class SubscriptionSyncProcessor {
53
61
  timestamp: new Date().toISOString(),
54
62
  });
55
63
  }
64
+ return { success: true };
56
65
  } catch (error) {
57
66
  const errorMsg = error instanceof Error ? error.message : String(error);
58
- store.setSyncStatus(SyncStatus.ERROR, errorMsg);
67
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
68
+ status: 'error',
69
+ phase: 'purchase',
70
+ userId: event.userId,
71
+ productId: event.productId,
72
+ error: errorMsg,
73
+ });
59
74
  console.error('[SubscriptionSyncProcessor] 🔴 PURCHASE FAILED', {
60
75
  userId: event.userId,
61
76
  productId: event.productId,
62
77
  error: errorMsg,
63
78
  timestamp: new Date().toISOString(),
64
79
  });
65
- throw error;
80
+ return { success: false, error: errorMsg };
66
81
  }
67
82
  }
68
83
 
69
- async handleRenewal(event: RenewalDetectedEvent): Promise<void> {
70
- const store = useSubscriptionFlowStore.getState();
71
- store.setSyncStatus(SyncStatus.SYNCING);
84
+ async handleRenewal(event: RenewalDetectedEvent): Promise<{ success: boolean; error?: string }> {
85
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
86
+ status: 'syncing',
87
+ phase: 'renewal',
88
+ userId: event.userId,
89
+ productId: event.productId,
90
+ });
72
91
 
73
92
  if (typeof __DEV__ !== "undefined" && __DEV__) {
74
93
  console.log('[SubscriptionSyncProcessor] 🔵 RENEWAL START', {
@@ -84,7 +103,12 @@ export class SubscriptionSyncProcessor {
84
103
  userId: event.userId,
85
104
  productId: event.productId,
86
105
  });
87
- store.setSyncStatus(SyncStatus.SUCCESS);
106
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
107
+ status: 'success',
108
+ phase: 'renewal',
109
+ userId: event.userId,
110
+ productId: event.productId,
111
+ });
88
112
  if (typeof __DEV__ !== "undefined" && __DEV__) {
89
113
  console.log('[SubscriptionSyncProcessor] 🟢 RENEWAL SUCCESS', {
90
114
  userId: event.userId,
@@ -92,20 +116,27 @@ export class SubscriptionSyncProcessor {
92
116
  timestamp: new Date().toISOString(),
93
117
  });
94
118
  }
119
+ return { success: true };
95
120
  } catch (error) {
96
121
  const errorMsg = error instanceof Error ? error.message : String(error);
97
- store.setSyncStatus(SyncStatus.ERROR, errorMsg);
122
+ subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED, {
123
+ status: 'error',
124
+ phase: 'renewal',
125
+ userId: event.userId,
126
+ productId: event.productId,
127
+ error: errorMsg,
128
+ });
98
129
  console.error('[SubscriptionSyncProcessor] 🔴 RENEWAL FAILED', {
99
130
  userId: event.userId,
100
131
  productId: event.productId,
101
132
  error: errorMsg,
102
133
  timestamp: new Date().toISOString(),
103
134
  });
104
- throw error;
135
+ return { success: false, error: errorMsg };
105
136
  }
106
137
  }
107
138
 
108
- async handlePremiumStatusChanged(event: PremiumStatusChangedEvent): Promise<void> {
139
+ async handlePremiumStatusChanged(event: PremiumStatusChangedEvent): Promise<{ success: boolean; error?: string }> {
109
140
  if (typeof __DEV__ !== "undefined" && __DEV__) {
110
141
  console.log('[SubscriptionSyncProcessor] 🔵 STATUS CHANGE START', {
111
142
  userId: event.userId,
@@ -130,6 +161,7 @@ export class SubscriptionSyncProcessor {
130
161
  timestamp: new Date().toISOString(),
131
162
  });
132
163
  }
164
+ return { success: true };
133
165
  } catch (error) {
134
166
  const errorMsg = error instanceof Error ? error.message : String(error);
135
167
  console.error('[SubscriptionSyncProcessor] 🔴 STATUS CHANGE FAILED', {
@@ -139,8 +171,8 @@ export class SubscriptionSyncProcessor {
139
171
  error: errorMsg,
140
172
  timestamp: new Date().toISOString(),
141
173
  });
142
- // We don't set global sync error here for passive status changes to avoid UI noise
143
- // throw error;
174
+ // We don't emit sync status change here for passive status changes to avoid UI noise
175
+ return { success: false, error: errorMsg };
144
176
  }
145
177
  }
146
178
 
@@ -23,9 +23,15 @@ export function configureServices(config: SubscriptionInitConfig, apiKey: string
23
23
  apiKey,
24
24
  entitlementIdentifier: entitlementId,
25
25
  consumableProductIdentifiers: [creditPackages.identifierPattern],
26
- onPurchaseCompleted: (event) => syncProcessor.handlePurchase(event),
27
- onRenewalDetected: (event) => syncProcessor.handleRenewal(event),
28
- onPremiumStatusChanged: (event) => syncProcessor.handlePremiumStatusChanged(event),
26
+ onPurchaseCompleted: async (event) => {
27
+ await syncProcessor.handlePurchase(event);
28
+ },
29
+ onRenewalDetected: async (event) => {
30
+ await syncProcessor.handleRenewal(event);
31
+ },
32
+ onPremiumStatusChanged: async (event) => {
33
+ await syncProcessor.handlePremiumStatusChanged(event);
34
+ },
29
35
  onCreditsUpdated,
30
36
  },
31
37
  apiKey,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Application flow events
3
+ * Events emitted during high-level application flow transitions
4
+ */
5
+
6
+ export const FLOW_EVENTS = {
7
+ ONBOARDING_COMPLETED: "flow_onboarding_completed",
8
+ PAYWALL_SHOWN: "flow_paywall_shown",
9
+ PAYWALL_CLOSED: "flow_paywall_closed",
10
+ } as const;
11
+
12
+ export type FlowEventType = typeof FLOW_EVENTS[keyof typeof FLOW_EVENTS];
13
+
14
+ export interface OnboardingCompletedEvent {
15
+ timestamp: number;
16
+ }
17
+
18
+ export interface PaywallShownEvent {
19
+ timestamp: number;
20
+ }
21
+
22
+ export interface PaywallClosedEvent {
23
+ timestamp: number;
24
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Subscription-related events
3
+ * Events emitted during subscription lifecycle operations
4
+ */
5
+
6
+ export const SUBSCRIPTION_EVENTS = {
7
+ CREDITS_UPDATED: "credits_updated",
8
+ PURCHASE_COMPLETED: "purchase_completed",
9
+ RENEWAL_DETECTED: "renewal_detected",
10
+ PREMIUM_STATUS_CHANGED: "premium_status_changed",
11
+ SYNC_STATUS_CHANGED: "sync_status_changed",
12
+ } as const;
13
+
14
+ export type SubscriptionEventType = typeof SUBSCRIPTION_EVENTS[keyof typeof SUBSCRIPTION_EVENTS];
15
+
16
+ export interface SyncStatusChangedEvent {
17
+ status: 'syncing' | 'success' | 'error';
18
+ phase: 'purchase' | 'renewal';
19
+ userId?: string;
20
+ productId?: string;
21
+ error?: string;
22
+ }
@@ -1,5 +1,6 @@
1
1
  import React, { createContext, useContext, useEffect } from "react";
2
2
  import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
3
+ import { useSyncStatusListener } from "../useSyncStatusListener";
3
4
  import { initializationState } from "../../infrastructure/state/initializationState";
4
5
 
5
6
  interface SubscriptionFlowContextType {
@@ -9,6 +10,9 @@ interface SubscriptionFlowContextType {
9
10
  const SubscriptionFlowContext = createContext<SubscriptionFlowContextType | undefined>(undefined);
10
11
 
11
12
  export const SubscriptionFlowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
13
+ // Listen to sync status events from application layer
14
+ useSyncStatusListener();
15
+
12
16
  // Selectors for stable references and only what we need
13
17
  const isInitialized = useSubscriptionFlowStore((state) => state.isInitialized);
14
18
  const isOnboardingComplete = useSubscriptionFlowStore((state) => state.isOnboardingComplete);
@@ -1,96 +1,33 @@
1
- import { useCallback, useMemo } from 'react';
2
- import type { PurchasesPackage } from 'react-native-purchases';
3
- import { useCredits } from '../../credits/presentation/useCredits';
4
- import { useSubscriptionStatus } from './useSubscriptionStatus';
5
- import {
6
- useSubscriptionPackages,
7
- usePurchasePackage,
8
- useRestorePurchase,
9
- } from '../infrastructure/hooks/useSubscriptionQueries';
10
- import { usePaywallVisibility } from './usePaywallVisibility';
11
- import { isPremiumSyncPending } from '../utils/syncStatus';
1
+ import { useMemo } from 'react';
2
+ import { usePremiumStatus } from './usePremiumStatus';
3
+ import { usePremiumPackages } from './usePremiumPackages';
4
+ import { usePremiumActions } from './usePremiumActions';
12
5
  import { UsePremiumResult } from './usePremium.types';
13
6
 
14
- const EMPTY_PACKAGES: PurchasesPackage[] = [];
15
-
7
+ /**
8
+ * Facade hook that combines status, packages, and actions.
9
+ *
10
+ * Consider using the focused hooks for better performance:
11
+ * - usePremiumStatus() - when you only need premium status
12
+ * - usePremiumPackages() - when you only need package data
13
+ * - usePremiumActions() - when you only need actions
14
+ *
15
+ * This facade re-renders when ANY of the sub-hooks change, whereas focused hooks
16
+ * only re-render when their specific data changes.
17
+ */
16
18
  export const usePremium = (): UsePremiumResult => {
17
- const { isPremium: subscriptionActive, isLoading: statusLoading } = useSubscriptionStatus();
18
- const { credits, isLoading: creditsLoading } = useCredits();
19
-
20
- const { data: packages = EMPTY_PACKAGES, isLoading: packagesLoading } = useSubscriptionPackages();
21
-
22
- const purchaseMutation = usePurchasePackage();
23
- const restoreMutation = useRestorePurchase();
24
-
25
- const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
26
-
27
- const isPremium = subscriptionActive || (credits?.isPremium ?? false);
28
- const isSyncing = isPremiumSyncPending({
29
- statusLoading,
30
- creditsLoading,
31
- subscriptionActive,
32
- credits,
33
- });
34
-
35
- const handlePurchase = useCallback(
36
- async (pkg: PurchasesPackage): Promise<boolean> => {
37
- try {
38
- const result = await purchaseMutation.mutateAsync(pkg);
39
- return result.success;
40
- } catch (error) {
41
- if (__DEV__) {
42
- console.error('[usePremium] Purchase failed:', error);
43
- }
44
- return false;
45
- }
46
- },
47
- [purchaseMutation],
48
- );
49
-
50
- const handleRestore = useCallback(async (): Promise<boolean> => {
51
- try {
52
- const result = await restoreMutation.mutateAsync();
53
- return result.success;
54
- } catch (error) {
55
- if (__DEV__) {
56
- console.error('[usePremium] Restore failed:', error);
57
- }
58
- return false;
59
- }
60
- }, [restoreMutation]);
19
+ const status = usePremiumStatus();
20
+ const packages = usePremiumPackages();
21
+ const actions = usePremiumActions();
61
22
 
62
23
  return useMemo(() => ({
63
- isPremium,
64
- isLoading:
65
- statusLoading ||
66
- creditsLoading ||
67
- packagesLoading ||
68
- purchaseMutation.isPending ||
69
- restoreMutation.isPending,
70
- packages,
71
- credits,
72
- showPaywall,
73
- isSyncing,
74
- purchasePackage: handlePurchase,
75
- restorePurchase: handleRestore,
76
- setShowPaywall,
77
- closePaywall,
78
- openPaywall,
24
+ ...status,
25
+ ...packages,
26
+ ...actions,
27
+ isLoading: status.isSyncing || packages.isLoading || actions.isLoading,
79
28
  }), [
80
- isPremium,
81
- statusLoading,
82
- creditsLoading,
83
- packagesLoading,
84
- purchaseMutation.isPending,
85
- restoreMutation.isPending,
29
+ status,
86
30
  packages,
87
- credits,
88
- showPaywall,
89
- isSyncing,
90
- handlePurchase,
91
- handleRestore,
92
- setShowPaywall,
93
- closePaywall,
94
- openPaywall,
31
+ actions,
95
32
  ]);
96
33
  };
@@ -0,0 +1,94 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import type { PurchasesPackage } from 'react-native-purchases';
3
+ import {
4
+ usePurchasePackage,
5
+ useRestorePurchase,
6
+ } from '../infrastructure/hooks/useSubscriptionQueries';
7
+ import { usePaywallVisibility } from './usePaywallVisibility';
8
+
9
+ export interface PremiumActions {
10
+ purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
11
+ restorePurchase: () => Promise<boolean>;
12
+ showPaywall: boolean;
13
+ setShowPaywall: (show: boolean) => void;
14
+ closePaywall: () => void;
15
+ openPaywall: () => void;
16
+ isPurchasing: boolean;
17
+ isRestoring: boolean;
18
+ isLoading: boolean;
19
+ isProductPurchasing: (productId: string) => boolean;
20
+ }
21
+
22
+ /**
23
+ * Hook for premium actions - mutations and visibility control.
24
+ *
25
+ * This hook is focused on user actions: purchasing, restoring, and paywall visibility.
26
+ * It does not include data fetching - use usePremiumStatus and usePremiumPackages for that.
27
+ *
28
+ * This separation allows components to only re-render when the specific state they care about changes.
29
+ */
30
+ export function usePremiumActions(): PremiumActions {
31
+ const purchaseMutation = usePurchasePackage();
32
+ const restoreMutation = useRestorePurchase();
33
+ const { showPaywall, setShowPaywall, closePaywall, openPaywall } = usePaywallVisibility();
34
+
35
+ const purchasePackage = useCallback(
36
+ async (pkg: PurchasesPackage): Promise<boolean> => {
37
+ try {
38
+ const result = await purchaseMutation.mutateAsync(pkg);
39
+ return result.success;
40
+ } catch (error) {
41
+ if (__DEV__) {
42
+ console.error('[usePremiumActions] Purchase failed:', error);
43
+ }
44
+ return false;
45
+ }
46
+ },
47
+ [purchaseMutation],
48
+ );
49
+
50
+ const restorePurchase = useCallback(async (): Promise<boolean> => {
51
+ try {
52
+ const result = await restoreMutation.mutateAsync();
53
+ return result.success;
54
+ } catch (error) {
55
+ if (__DEV__) {
56
+ console.error('[usePremiumActions] Restore failed:', error);
57
+ }
58
+ return false;
59
+ }
60
+ }, [restoreMutation]);
61
+
62
+ const isPurchasing = purchaseMutation.isPending;
63
+ const isRestoring = restoreMutation.isPending;
64
+ const isLoading = isPurchasing || isRestoring;
65
+
66
+ const isProductPurchasing = useCallback((productId: string): boolean => {
67
+ return purchaseMutation.variables?.product?.identifier === productId &&
68
+ purchaseMutation.isPending;
69
+ }, [purchaseMutation]);
70
+
71
+ return useMemo(() => ({
72
+ purchasePackage,
73
+ restorePurchase,
74
+ showPaywall,
75
+ setShowPaywall,
76
+ closePaywall,
77
+ openPaywall,
78
+ isPurchasing,
79
+ isRestoring,
80
+ isLoading,
81
+ isProductPurchasing,
82
+ }), [
83
+ purchasePackage,
84
+ restorePurchase,
85
+ showPaywall,
86
+ setShowPaywall,
87
+ closePaywall,
88
+ openPaywall,
89
+ isPurchasing,
90
+ isRestoring,
91
+ isLoading,
92
+ isProductPurchasing,
93
+ ]);
94
+ }
@@ -0,0 +1,25 @@
1
+ import { useMemo } from 'react';
2
+ import type { PurchasesPackage } from 'react-native-purchases';
3
+ import { useSubscriptionPackages } from '../infrastructure/hooks/useSubscriptionQueries';
4
+
5
+ const EMPTY_PACKAGES: PurchasesPackage[] = [];
6
+
7
+ export interface PremiumPackages {
8
+ packages: PurchasesPackage[];
9
+ isLoading: boolean;
10
+ }
11
+
12
+ /**
13
+ * Hook for fetching subscription packages.
14
+ *
15
+ * This hook is focused solely on package data - no premium status or mutations.
16
+ * Use this when you only need package information for display purposes.
17
+ */
18
+ export function usePremiumPackages(): PremiumPackages {
19
+ const { data: packages = EMPTY_PACKAGES, isLoading } = useSubscriptionPackages();
20
+
21
+ return useMemo(() => ({
22
+ packages,
23
+ isLoading,
24
+ }), [packages, isLoading]);
25
+ }
@@ -0,0 +1,38 @@
1
+ import { useMemo } from 'react';
2
+ import type { UserCredits } from '../../credits/core/Credits';
3
+ import { useCredits } from '../../credits/presentation/useCredits';
4
+ import { useSubscriptionStatus } from './useSubscriptionStatus';
5
+ import { isPremiumSyncPending } from '../utils/syncStatus';
6
+
7
+ export interface PremiumStatus {
8
+ isPremium: boolean;
9
+ credits: UserCredits | null;
10
+ isSyncing: boolean;
11
+ }
12
+
13
+ /**
14
+ * Read-only hook for premium status.
15
+ * Combines subscription status and credits to determine overall premium state.
16
+ *
17
+ * This hook is focused on data reading only - no mutations or side effects.
18
+ * Use this when you only need to know the premium status without triggering
19
+ * purchases or other actions.
20
+ */
21
+ export function usePremiumStatus(): PremiumStatus {
22
+ const { isPremium: subscriptionActive, isLoading: statusLoading } = useSubscriptionStatus();
23
+ const { credits, isLoading: creditsLoading } = useCredits();
24
+
25
+ const isPremium = subscriptionActive || (credits?.isPremium ?? false);
26
+ const isSyncing = isPremiumSyncPending({
27
+ statusLoading,
28
+ creditsLoading,
29
+ subscriptionActive,
30
+ credits,
31
+ });
32
+
33
+ return useMemo(() => ({
34
+ isPremium,
35
+ credits,
36
+ isSyncing,
37
+ }), [isPremium, credits, isSyncing]);
38
+ }
@@ -4,8 +4,8 @@
4
4
  * Uses @umituz/react-native-design-system's storage utility for standardized persistence.
5
5
  */
6
6
 
7
- import { DeviceEventEmitter } from "react-native";
8
7
  import { createStore } from "@umituz/react-native-design-system/storage";
8
+ import { subscriptionEventBus, FLOW_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
9
9
 
10
10
  export enum SubscriptionFlowStatus {
11
11
  INITIALIZING = "INITIALIZING",
@@ -77,7 +77,7 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
77
77
  showPostOnboardingPaywall: true,
78
78
  status: SubscriptionFlowStatus.POST_ONBOARDING_PAYWALL,
79
79
  });
80
- DeviceEventEmitter.emit("onboarding-complete");
80
+ subscriptionEventBus.emit(FLOW_EVENTS.ONBOARDING_COMPLETED, { timestamp: Date.now() });
81
81
  },
82
82
  closePostOnboardingPaywall: async () => {
83
83
  set({
@@ -85,11 +85,15 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
85
85
  paywallShown: true,
86
86
  status: SubscriptionFlowStatus.READY,
87
87
  });
88
+ subscriptionEventBus.emit(FLOW_EVENTS.PAYWALL_CLOSED, { timestamp: Date.now() });
88
89
  },
89
90
  closeFeedback: () => set({ showFeedback: false }),
90
91
  setAuthModalOpen: (open: boolean) => set({ isAuthModalOpen: open }),
91
92
  setShowFeedback: (show: boolean) => set({ showFeedback: show }),
92
- markPaywallShown: async () => set({ paywallShown: true }),
93
+ markPaywallShown: async () => {
94
+ set({ paywallShown: true });
95
+ subscriptionEventBus.emit(FLOW_EVENTS.PAYWALL_SHOWN, { timestamp: Date.now() });
96
+ },
93
97
  setInitialized: (initialized: boolean) => set((state) => {
94
98
  if (state.isInitialized === initialized) return state;
95
99
  return { isInitialized: initialized };
@@ -0,0 +1,47 @@
1
+ import { useEffect } from "react";
2
+ import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
3
+ import { useSubscriptionFlowStore, SyncStatus } from "./useSubscriptionFlow";
4
+
5
+ interface SyncStatusEvent {
6
+ status: 'syncing' | 'success' | 'error';
7
+ phase: 'purchase' | 'renewal';
8
+ userId?: string;
9
+ productId?: string;
10
+ error?: string;
11
+ }
12
+
13
+ /**
14
+ * Hook that listens to sync status events from the application layer
15
+ * and updates the presentation layer's Zustand store accordingly.
16
+ *
17
+ * This maintains clean architecture separation: Application layer emits events,
18
+ * Presentation layer consumes them and updates its own state.
19
+ */
20
+ export function useSyncStatusListener() {
21
+ const setSyncStatus = useSubscriptionFlowStore((state) => state.setSyncStatus);
22
+
23
+ useEffect(() => {
24
+ const unsubscribe = subscriptionEventBus.on<SyncStatusEvent>(
25
+ SUBSCRIPTION_EVENTS.SYNC_STATUS_CHANGED,
26
+ (event) => {
27
+ const syncStatus = event.status === 'syncing' ? SyncStatus.SYNCING :
28
+ event.status === 'success' ? SyncStatus.SUCCESS :
29
+ SyncStatus.ERROR;
30
+
31
+ setSyncStatus(syncStatus, event.error);
32
+
33
+ if (__DEV__) {
34
+ console.log('[useSyncStatusListener] Sync status updated', {
35
+ status: event.status,
36
+ phase: event.phase,
37
+ userId: event.userId,
38
+ productId: event.productId,
39
+ error: event.error,
40
+ });
41
+ }
42
+ }
43
+ );
44
+
45
+ return unsubscribe;
46
+ }, [setSyncStatus]);
47
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,13 @@ export {
9
9
  PURCHASE_TYPE,
10
10
  ANONYMOUS_CACHE_KEY,
11
11
  } from "./domains/subscription/core/SubscriptionConstants";
12
+
13
+ // Domain Events
14
+ export { SUBSCRIPTION_EVENTS } from "./domains/subscription/core/events/SubscriptionEvents";
15
+ export type { SubscriptionEventType, SyncStatusChangedEvent } from "./domains/subscription/core/events/SubscriptionEvents";
16
+ export type { PurchaseCompletedEvent, RenewalDetectedEvent, PremiumStatusChangedEvent } from "./domains/subscription/core/SubscriptionEvents";
17
+ export { FLOW_EVENTS } from "./domains/subscription/core/events/FlowEvents";
18
+ export type { FlowEventType, OnboardingCompletedEvent, PaywallShownEvent, PaywallClosedEvent } from "./domains/subscription/core/events/FlowEvents";
12
19
  export type {
13
20
  UserTierType,
14
21
  SubscriptionStatusType,
@@ -19,7 +26,7 @@ export type {
19
26
  PurchaseType,
20
27
  } from "./domains/subscription/core/SubscriptionConstants";
21
28
  export type { SubscriptionMetadata } from "./domains/subscription/core/types/SubscriptionMetadata";
22
- export type { PremiumStatus } from "./domains/subscription/core/types/PremiumStatus";
29
+ export type { PremiumStatus as PremiumStatusMetadata } from "./domains/subscription/core/types/PremiumStatus";
23
30
  export type { CreditInfo } from "./domains/subscription/core/types/CreditInfo";
24
31
  export {
25
32
  createDefaultSubscriptionStatus,
@@ -63,10 +70,18 @@ export { useDeductCredit } from "./domains/credits/presentation/deduct-credit/us
63
70
  export { useFeatureGate } from "./domains/subscription/presentation/useFeatureGate";
64
71
  export { usePaywallVisibility, paywallControl } from "./domains/subscription/presentation/usePaywallVisibility";
65
72
  export { usePremium } from "./domains/subscription/presentation/usePremium";
73
+ export { usePremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
74
+ export { usePremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
75
+ export { usePremiumActions } from "./domains/subscription/presentation/usePremiumActions";
76
+ export type { UsePremiumResult } from "./domains/subscription/presentation/usePremium.types";
77
+ export type { PremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
78
+ export type { PremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
79
+ export type { PremiumActions } from "./domains/subscription/presentation/usePremiumActions";
66
80
  export { useSubscriptionFlowStore } from "./domains/subscription/presentation/useSubscriptionFlow";
67
81
  export type { SubscriptionFlowState, SubscriptionFlowActions, SubscriptionFlowStore } from "./domains/subscription/presentation/useSubscriptionFlow";
68
82
  export { useSubscriptionStatus } from "./domains/subscription/presentation/useSubscriptionStatus";
69
83
  export type { SubscriptionStatusResult } from "./domains/subscription/presentation/useSubscriptionStatus.types";
84
+ export { useSyncStatusListener } from "./domains/subscription/presentation/useSyncStatusListener";
70
85
  export { usePaywallFeedback } from "./presentation/hooks/feedback/usePaywallFeedback";
71
86
  export {
72
87
  usePaywallFeedbackSubmit,
@@ -108,6 +123,7 @@ export type {
108
123
  CreditsResult,
109
124
  DeductCreditsResult,
110
125
  } from "./domains/credits/core/Credits";
126
+ export { CreditLimitService, calculateCreditLimit } from "./domains/credits/domain/services/CreditLimitService";
111
127
 
112
128
  // Utils
113
129
  export {
@@ -167,10 +183,6 @@ export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presen
167
183
  export { SubscriptionFlowStatus } from "./domains/subscription/presentation/useSubscriptionFlow";
168
184
  export { SubscriptionFlowProvider, useSubscriptionFlowStatus } from "./domains/subscription/presentation/providers/SubscriptionFlowProvider";
169
185
 
170
- // Purchase Loading Overlay
171
- export { PurchaseLoadingOverlay } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
172
- export type { PurchaseLoadingOverlayProps } from "./domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay";
173
-
174
186
  // Init Module Factory
175
187
  export {
176
188
  createSubscriptionInitModule,
@@ -17,7 +17,7 @@ class SubscriptionEventBus {
17
17
  if (!this.listeners.has(event)) {
18
18
  this.listeners.set(event, new Set());
19
19
  }
20
-
20
+
21
21
  const eventSet = this.listeners.get(event)!;
22
22
  eventSet.add(callback as EventCallback);
23
23
 
@@ -36,8 +36,6 @@ class SubscriptionEventBus {
36
36
  const listeners = this.listeners.get(event);
37
37
  if (!listeners || listeners.size === 0) return;
38
38
 
39
- // Use microtask for async execution to not block main thread
40
- // but keep it fast.
41
39
  listeners.forEach(callback => {
42
40
  queueMicrotask(() => {
43
41
  try {
@@ -71,9 +69,9 @@ class SubscriptionEventBus {
71
69
 
72
70
  export const subscriptionEventBus = SubscriptionEventBus.getInstance();
73
71
 
74
- export const SUBSCRIPTION_EVENTS = {
75
- CREDITS_UPDATED: "credits_updated",
76
- PURCHASE_COMPLETED: "purchase_completed",
77
- RENEWAL_DETECTED: "renewal_detected",
78
- PREMIUM_STATUS_CHANGED: "premium_status_changed",
79
- };
72
+ // Re-export event constants for external use
73
+ export { SUBSCRIPTION_EVENTS } from "../../domains/subscription/core/events/SubscriptionEvents";
74
+ export { FLOW_EVENTS } from "../../domains/subscription/core/events/FlowEvents";
75
+ export type { SubscriptionEventType } from "../../domains/subscription/core/events/SubscriptionEvents";
76
+ export type { FlowEventType } from "../../domains/subscription/core/events/FlowEvents";
77
+
@@ -36,6 +36,3 @@ export const LONG_CACHE_CONFIG = {
36
36
  refetchOnWindowFocus: false,
37
37
  refetchOnReconnect: true,
38
38
  };
39
-
40
- /** @deprecated Use SHORT_CACHE_CONFIG instead */
41
- export const NO_CACHE_QUERY_CONFIG = SHORT_CACHE_CONFIG;
@@ -1,10 +0,0 @@
1
- import type { CreditsConfig } from "../core/Credits";
2
- import { calculateCreditLimit as calculateLimit } from "../utils/creditCalculations";
3
-
4
- /**
5
- * Service to calculate credit limits based on product configuration.
6
- * Uses centralized utility functions for calculations.
7
- */
8
- export function calculateCreditLimit(productId: string | undefined, config: CreditsConfig): number {
9
- return calculateLimit(productId, config);
10
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Purchase Loading Overlay
3
- * Full-screen overlay shown during purchase operations
4
- * Locks the UI and shows a spinner with optional message
5
- */
6
-
7
- import React from "react";
8
- import { View, Modal, StyleSheet } from "react-native";
9
- import { AtomicSpinner, AtomicText } from "@umituz/react-native-design-system/atoms";
10
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
- import { usePurchaseLoadingStore, selectIsPurchasing } from "../../stores/purchaseLoadingStore";
12
- import type { PurchaseLoadingOverlayProps } from "./PurchaseLoadingOverlay.types";
13
-
14
- export type { PurchaseLoadingOverlayProps };
15
-
16
- export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
17
- ({ loadingText }) => {
18
- const tokens = useAppDesignTokens();
19
- const isPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
20
-
21
- return (
22
- <Modal visible={isPurchasing} transparent animationType="none" statusBarTranslucent>
23
- <View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
24
- <View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
25
- <AtomicSpinner size="lg" color="primary" />
26
- {loadingText && (
27
- <AtomicText
28
- type="bodyLarge"
29
- style={[styles.text, { color: tokens.colors.textPrimary }]}
30
- >
31
- {loadingText}
32
- </AtomicText>
33
- )}
34
- </View>
35
- </View>
36
- </Modal>
37
- );
38
- }
39
- );
40
-
41
- PurchaseLoadingOverlay.displayName = "PurchaseLoadingOverlay";
42
-
43
- const styles = StyleSheet.create({
44
- container: {
45
- flex: 1,
46
- justifyContent: "center",
47
- alignItems: "center",
48
- },
49
- content: {
50
- paddingHorizontal: 32,
51
- paddingVertical: 24,
52
- borderRadius: 16,
53
- alignItems: "center",
54
- minWidth: 200,
55
- },
56
- text: {
57
- marginTop: 16,
58
- textAlign: "center",
59
- },
60
- });
@@ -1,4 +0,0 @@
1
- export interface PurchaseLoadingOverlayProps {
2
- /** Loading message to display */
3
- loadingText?: string;
4
- }
@@ -1,53 +0,0 @@
1
- import { create } from "zustand";
2
-
3
- interface PurchaseLoadingState {
4
- activePurchases: Map<string, "manual" | "auto-execution">;
5
- }
6
-
7
- interface PurchaseLoadingActions {
8
- startPurchase: (productId: string, source: "manual" | "auto-execution") => void;
9
- endPurchase: (productId: string) => void;
10
- reset: () => void;
11
- }
12
-
13
- type PurchaseLoadingStore = PurchaseLoadingState & PurchaseLoadingActions;
14
-
15
- const createInitialState = (): PurchaseLoadingState => ({
16
- activePurchases: new Map(),
17
- });
18
-
19
- export const usePurchaseLoadingStore = create<PurchaseLoadingStore>((set) => ({
20
- ...createInitialState(),
21
-
22
- startPurchase: (productId, source) => {
23
- set((state) => {
24
- const newPurchases = new Map(state.activePurchases);
25
- newPurchases.set(productId, source);
26
- return { activePurchases: newPurchases };
27
- });
28
- },
29
-
30
- endPurchase: (productId) => {
31
- set((state) => {
32
- const newPurchases = new Map(state.activePurchases);
33
- newPurchases.delete(productId);
34
- return { activePurchases: newPurchases };
35
- });
36
- },
37
-
38
- reset: () => {
39
- set(createInitialState());
40
- },
41
- }));
42
-
43
- /**
44
- * Optimized selector for purchasing state.
45
- * Use this to avoid re-renders when other parts of the state change.
46
- */
47
- export const selectIsPurchasing = (state: PurchaseLoadingStore) => state.activePurchases.size > 0;
48
-
49
- /**
50
- * Optimized selector for a specific product's purchasing state.
51
- */
52
- export const selectIsProductPurchasing = (productId: string) => (state: PurchaseLoadingStore) =>
53
- state.activePurchases.has(productId);