@umituz/react-native-subscription 3.1.9 → 3.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/presentation/useCreditsRealTime.ts +31 -73
  3. package/src/domains/credits/utils/creditValidation.ts +5 -26
  4. package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
  5. package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
  6. package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
  7. package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
  8. package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
  9. package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
  10. package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
  11. package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
  12. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
  13. package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
  14. package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
  15. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
  16. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
  17. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
  18. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
  19. package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
  20. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
  21. package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
  22. package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
  23. package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
  24. package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
  25. package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
  26. package/src/domains/subscription/presentation/flowInitialState.ts +22 -0
  27. package/src/domains/subscription/presentation/flowTypes.ts +106 -0
  28. package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
  29. package/src/domains/subscription/presentation/usePremiumActions.ts +5 -6
  30. package/src/domains/subscription/presentation/useSubscriptionFlow.ts +25 -92
  31. package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
  32. package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
  33. package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +34 -60
  34. package/src/index.components.ts +1 -1
  35. package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
  36. package/src/shared/presentation/hooks/useFirestoreRealTime.ts +230 -0
  37. package/src/shared/presentation/types/hookState.types.ts +97 -0
  38. package/src/shared/utils/errors/errorAssertions.ts +35 -0
  39. package/src/shared/utils/errors/errorConversion.ts +73 -0
  40. package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
  41. package/src/shared/utils/errors/errorWrappers.ts +54 -0
  42. package/src/shared/utils/errors/index.ts +19 -0
  43. package/src/shared/utils/errors/serviceErrors.ts +36 -0
  44. package/src/shared/utils/logger.ts +140 -0
  45. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
@@ -11,6 +11,12 @@ interface SubscriptionHeaderContentStyles {
11
11
  value: TextStyle;
12
12
  }
13
13
 
14
+ interface DetailInfo {
15
+ label?: string;
16
+ value: string;
17
+ highlight?: boolean;
18
+ }
19
+
14
20
  interface SubscriptionHeaderContentProps {
15
21
  showExpirationDate: boolean;
16
22
  expirationDate?: string;
@@ -28,106 +34,116 @@ interface SubscriptionHeaderContentProps {
28
34
  isSandbox?: boolean;
29
35
  }
30
36
 
31
- export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps> = ({
32
- showExpirationDate,
33
- expirationDate,
34
- purchaseDate,
35
- showExpiring,
36
- translations,
37
- styles,
38
- willRenew,
39
- periodType: _periodType,
40
- packageType,
41
- store,
42
- originalPurchaseDate,
43
- latestPurchaseDate,
44
- billingIssuesDetected,
45
- isSandbox,
46
- }) => (
47
- <View style={styles.details}>
48
- {showExpirationDate && expirationDate && (
49
- <DetailRow
50
- label={translations.expiresLabel}
51
- value={expirationDate}
52
- highlight={showExpiring}
53
- style={styles.row}
54
- labelStyle={styles.label}
55
- valueStyle={styles.value}
56
- />
57
- )}
58
- {purchaseDate && (
59
- <DetailRow
60
- label={translations.purchasedLabel}
61
- value={purchaseDate}
62
- style={styles.row}
63
- labelStyle={styles.label}
64
- valueStyle={styles.value}
65
- />
66
- )}
67
- {willRenew !== null && willRenew !== undefined && translations.willRenewLabel && (
68
- <DetailRow
69
- label={translations.willRenewLabel}
70
- value={willRenew ? (translations.willRenewYes ?? "Yes") : (translations.willRenewNo ?? "No")}
71
- highlight={!willRenew}
72
- style={styles.row}
73
- labelStyle={styles.label}
74
- valueStyle={styles.value}
75
- />
76
- )}
77
- {packageType && translations.periodTypeLabel && (
78
- <DetailRow
79
- label={translations.periodTypeLabel}
80
- value={formatPackageTypeForDisplay(packageType)}
81
- style={styles.row}
82
- labelStyle={styles.label}
83
- valueStyle={styles.value}
84
- />
85
- )}
86
- {store && translations.storeLabel && (
87
- <DetailRow
88
- label={translations.storeLabel}
89
- value={store}
90
- style={styles.row}
91
- labelStyle={styles.label}
92
- valueStyle={styles.value}
93
- />
94
- )}
95
- {originalPurchaseDate && translations.originalPurchaseDateLabel && (
96
- <DetailRow
97
- label={translations.originalPurchaseDateLabel}
98
- value={originalPurchaseDate}
99
- style={styles.row}
100
- labelStyle={styles.label}
101
- valueStyle={styles.value}
102
- />
103
- )}
104
- {latestPurchaseDate && translations.latestPurchaseDateLabel && (
105
- <DetailRow
106
- label={translations.latestPurchaseDateLabel}
107
- value={latestPurchaseDate}
108
- style={styles.row}
109
- labelStyle={styles.label}
110
- valueStyle={styles.value}
111
- />
112
- )}
113
- {billingIssuesDetected && translations.billingIssuesLabel && (
114
- <DetailRow
115
- label={translations.billingIssuesLabel}
116
- value={translations.billingIssuesDetected ?? "Detected"}
117
- highlight={true}
118
- style={styles.row}
119
- labelStyle={styles.label}
120
- valueStyle={styles.value}
121
- />
122
- )}
123
- {typeof __DEV__ !== 'undefined' && __DEV__ && isSandbox && translations.sandboxLabel && (
124
- <DetailRow
125
- label={translations.sandboxLabel}
126
- value={translations.sandboxTestMode ?? "Test Mode"}
127
- style={styles.row}
128
- labelStyle={styles.label}
129
- valueStyle={styles.value}
130
- />
131
- )}
132
- </View>
133
- );
37
+ /**
38
+ * Helper to build detail row data array.
39
+ * Reduces code duplication by centralizing detail row creation logic.
40
+ */
41
+ function buildDetails(
42
+ props: SubscriptionHeaderContentProps
43
+ ): DetailInfo[] {
44
+ const {
45
+ showExpirationDate,
46
+ expirationDate,
47
+ purchaseDate,
48
+ showExpiring,
49
+ translations,
50
+ willRenew,
51
+ packageType,
52
+ store,
53
+ originalPurchaseDate,
54
+ latestPurchaseDate,
55
+ billingIssuesDetected,
56
+ isSandbox,
57
+ } = props;
58
+
59
+ const details: DetailInfo[] = [];
60
+
61
+ if (showExpirationDate && expirationDate) {
62
+ details.push({
63
+ label: translations.expiresLabel,
64
+ value: expirationDate,
65
+ highlight: showExpiring,
66
+ });
67
+ }
68
+
69
+ if (purchaseDate) {
70
+ details.push({
71
+ label: translations.purchasedLabel,
72
+ value: purchaseDate,
73
+ });
74
+ }
75
+
76
+ if (willRenew !== null && willRenew !== undefined && translations.willRenewLabel) {
77
+ details.push({
78
+ label: translations.willRenewLabel,
79
+ value: willRenew ? (translations.willRenewYes ?? "Yes") : (translations.willRenewNo ?? "No"),
80
+ highlight: !willRenew,
81
+ });
82
+ }
83
+
84
+ if (packageType && translations.periodTypeLabel) {
85
+ details.push({
86
+ label: translations.periodTypeLabel,
87
+ value: formatPackageTypeForDisplay(packageType),
88
+ });
89
+ }
90
+
91
+ if (store && translations.storeLabel) {
92
+ details.push({
93
+ label: translations.storeLabel,
94
+ value: store,
95
+ });
96
+ }
97
+
98
+ if (originalPurchaseDate && translations.originalPurchaseDateLabel) {
99
+ details.push({
100
+ label: translations.originalPurchaseDateLabel,
101
+ value: originalPurchaseDate,
102
+ });
103
+ }
104
+
105
+ if (latestPurchaseDate && translations.latestPurchaseDateLabel) {
106
+ details.push({
107
+ label: translations.latestPurchaseDateLabel,
108
+ value: latestPurchaseDate,
109
+ });
110
+ }
111
+
112
+ if (billingIssuesDetected && translations.billingIssuesLabel) {
113
+ details.push({
114
+ label: translations.billingIssuesLabel,
115
+ value: translations.billingIssuesDetected ?? "Detected",
116
+ highlight: true,
117
+ });
118
+ }
119
+
120
+ if (typeof __DEV__ !== 'undefined' && __DEV__ && isSandbox && translations.sandboxLabel) {
121
+ details.push({
122
+ label: translations.sandboxLabel,
123
+ value: translations.sandboxTestMode ?? "Test Mode",
124
+ });
125
+ }
126
+
127
+ return details;
128
+ }
129
+
130
+ export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps> = (props) => {
131
+ const { styles } = props;
132
+ const details = buildDetails(props);
133
+
134
+ return (
135
+ <View style={styles.details}>
136
+ {details.map((detail) => (
137
+ <DetailRow
138
+ key={detail.label}
139
+ label={detail.label!}
140
+ value={detail.value}
141
+ highlight={detail.highlight}
142
+ style={styles.row}
143
+ labelStyle={styles.label}
144
+ valueStyle={styles.value}
145
+ />
146
+ ))}
147
+ </View>
148
+ );
149
+ };
@@ -5,6 +5,9 @@ import {
5
5
  useRestorePurchase,
6
6
  } from '../infrastructure/hooks/useSubscriptionQueries';
7
7
  import { usePaywallVisibility } from './usePaywallVisibility';
8
+ import { createLogger } from '../../../shared/utils/logger';
9
+
10
+ const logger = createLogger('usePremiumActions');
8
11
 
9
12
  export interface PremiumActions {
10
13
  purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
@@ -38,9 +41,7 @@ export function usePremiumActions(): PremiumActions {
38
41
  const result = await purchaseMutation.mutateAsync(pkg);
39
42
  return result.success;
40
43
  } catch (error) {
41
- if (__DEV__) {
42
- console.error('[usePremiumActions] Purchase failed:', error);
43
- }
44
+ logger.error('Purchase failed', error, { packageId: pkg.identifier });
44
45
  return false;
45
46
  }
46
47
  },
@@ -52,9 +53,7 @@ export function usePremiumActions(): PremiumActions {
52
53
  const result = await restoreMutation.mutateAsync();
53
54
  return result.success;
54
55
  } catch (error) {
55
- if (__DEV__) {
56
- console.error('[usePremiumActions] Restore failed:', error);
57
- }
56
+ logger.error('Restore failed', error);
58
57
  return false;
59
58
  }
60
59
  }, [restoreMutation]);
@@ -3,109 +3,35 @@
3
3
  *
4
4
  * Single source of truth for app flow state.
5
5
  * Clean state transitions without complex if/else logic.
6
+ *
7
+ * State transition rules:
8
+ * - INITIALIZING -> ONBOARDING (first launch)
9
+ * - INITIALIZING -> CHECK_PREMIUM (onboarding already done)
10
+ * - ONBOARDING -> CHECK_PREMIUM (onboarding completed)
11
+ * - CHECK_PREMIUM -> READY (user is premium)
12
+ * - CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
13
+ * - CHECK_PREMIUM -> READY (user not premium but paywall already shown)
14
+ * - POST_ONBOARDING_PAYWALL -> READY (paywall closed)
15
+ * - READY -> READY (stays ready, shows overlays when needed)
6
16
  */
7
17
 
8
18
  import { createStore } from "@umituz/react-native-design-system/storage";
9
19
  import { subscriptionEventBus, FLOW_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
20
+ import {
21
+ SubscriptionFlowStatus,
22
+ SyncStatus,
23
+ type SubscriptionFlowState,
24
+ type SubscriptionFlowActions,
25
+ } from "./flowTypes";
26
+ import { initialFlowState } from "./flowInitialState";
10
27
 
11
- export enum SubscriptionFlowStatus {
12
- INITIALIZING = "INITIALIZING",
13
- ONBOARDING = "ONBOARDING",
14
- CHECK_PREMIUM = "CHECK_PREMIUM",
15
- POST_ONBOARDING_PAYWALL = "POST_ONBOARDING_PAYWALL",
16
- READY = "READY",
17
- }
18
-
19
- export enum SyncStatus {
20
- IDLE = "IDLE",
21
- SYNCING = "SYNCING",
22
- SUCCESS = "SUCCESS",
23
- ERROR = "ERROR",
24
- }
25
-
26
- export interface SubscriptionFlowState {
27
- // Flow state
28
- status: SubscriptionFlowStatus;
29
-
30
- // Sync state
31
- syncStatus: SyncStatus;
32
- syncError: string | null;
33
-
34
- // Onboarding state
35
- isOnboardingComplete: boolean;
36
-
37
- // Paywall state
38
- paywallShown: boolean;
39
-
40
- // Feedback state
41
- showFeedback: boolean;
42
-
43
- // Auth modal state
44
- isAuthModalOpen: boolean;
45
-
46
- // Initialization flag
47
- isInitialized: boolean;
48
- }
49
-
50
- export interface SubscriptionFlowActions {
51
- // Flow actions
52
- completeOnboarding: () => void;
53
- showPaywall: () => void;
54
- completePaywall: (purchased: boolean) => void;
55
- showFeedbackScreen: () => void;
56
- hideFeedback: () => void;
57
-
58
- // Auth actions
59
- setAuthModalOpen: (open: boolean) => void;
60
-
61
- // Sync actions
62
- setSyncStatus: (status: SyncStatus, error?: string | null) => void;
63
-
64
- // State setters (for internal use)
65
- setInitialized: (initialized: boolean) => void;
66
- setStatus: (status: SubscriptionFlowStatus) => void;
67
-
68
- // Reset
69
- resetFlow: () => void;
70
- }
71
-
72
- export type SubscriptionFlowStore = SubscriptionFlowState & SubscriptionFlowActions;
73
-
74
- const initialState: SubscriptionFlowState = {
75
- status: SubscriptionFlowStatus.INITIALIZING,
76
- syncStatus: SyncStatus.IDLE,
77
- syncError: null,
78
- isOnboardingComplete: false,
79
- paywallShown: false,
80
- showFeedback: false,
81
- isAuthModalOpen: false,
82
- isInitialized: false,
83
- };
84
-
85
- /**
86
- * State transition rules:
87
- *
88
- * INITIALIZING -> ONBOARDING (first launch)
89
- * INITIALIZING -> CHECK_PREMIUM (onboarding already done)
90
- *
91
- * ONBOARDING -> CHECK_PREMIUM (onboarding completed)
92
- *
93
- * CHECK_PREMIUM -> READY (user is premium)
94
- * CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
95
- * CHECK_PREMIUM -> READY (user not premium but paywall already shown)
96
- *
97
- * POST_ONBOARDING_PAYWALL -> READY (paywall closed)
98
- *
99
- * READY -> READY (stays ready, shows overlays when needed)
100
- */
101
28
  export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, SubscriptionFlowActions>({
102
29
  name: "subscription-flow-storage",
103
- initialState,
30
+ initialState: initialFlowState,
104
31
  persist: true,
105
32
  onRehydrate: (state) => {
106
33
  if (!state.isInitialized) {
107
34
  state.setInitialized(true);
108
-
109
35
  // First time: show onboarding
110
36
  state.setStatus(SubscriptionFlowStatus.INITIALIZING);
111
37
  } else if (state.isOnboardingComplete) {
@@ -176,3 +102,10 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
176
102
  },
177
103
  }),
178
104
  });
105
+
106
+ // Re-export types for convenience
107
+ export type { SubscriptionFlowState, SubscriptionFlowActions } from "./flowTypes";
108
+ export { SubscriptionFlowStatus, SyncStatus } from "./flowTypes";
109
+
110
+ // Re-export store type inferred from createStore
111
+ export type SubscriptionFlowStore = ReturnType<typeof useSubscriptionFlowStore>;
@@ -58,6 +58,13 @@ export const BalanceCard: React.FC<BalanceCardProps> = React.memo(({
58
58
  </View>
59
59
  </View>
60
60
  );
61
+ }, (prevProps, nextProps) => {
62
+ // PERFORMANCE: Custom comparison to prevent unnecessary re-renders
63
+ return (
64
+ prevProps.balance === nextProps.balance &&
65
+ prevProps.translations === nextProps.translations &&
66
+ prevProps.iconName === nextProps.iconName
67
+ );
61
68
  });
62
69
 
63
70
  const styles = StyleSheet.create({
@@ -44,4 +44,15 @@ export const TransactionItem: React.FC<TransactionItemProps> = React.memo(({
44
44
  </AtomicText>
45
45
  </View>
46
46
  );
47
+ }, (prevProps, nextProps) => {
48
+ // PERFORMANCE: Custom comparison to prevent unnecessary re-renders
49
+ return (
50
+ prevProps.transaction.id === nextProps.transaction.id &&
51
+ prevProps.transaction.change === nextProps.transaction.change &&
52
+ prevProps.transaction.reason === nextProps.transaction.reason &&
53
+ prevProps.transaction.description === nextProps.transaction.description &&
54
+ prevProps.transaction.createdAt === nextProps.transaction.createdAt &&
55
+ prevProps.translations === nextProps.translations &&
56
+ prevProps.dateFormatter === nextProps.dateFormatter
57
+ );
47
58
  });
@@ -1,11 +1,12 @@
1
- import { useState, useEffect } from "react";
1
+ import { useMemo } from "react";
2
2
  import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
3
- import { collection, onSnapshot, query, orderBy, limit, Query } from "firebase/firestore";
3
+ import { collection, query, orderBy, limit } from "firebase/firestore";
4
4
  import type {
5
5
  CreditLog,
6
6
  TransactionRepositoryConfig,
7
7
  } from "../../domain/types/transaction.types";
8
8
  import { requireFirestore } from "../../../../shared/infrastructure/firestore/collectionUtils";
9
+ import { useFirestoreCollectionRealTime } from "../../../../shared/presentation/hooks/useFirestoreRealTime";
9
10
 
10
11
  export interface UseTransactionHistoryParams {
11
12
  config: TransactionRepositoryConfig;
@@ -20,78 +21,51 @@ interface UseTransactionHistoryResult {
20
21
  isEmpty: boolean;
21
22
  }
22
23
 
24
+ /**
25
+ * Mapper to convert Firestore document to CreditLog entity.
26
+ */
27
+ function mapTransactionLog(doc: any, docId: string): CreditLog {
28
+ return {
29
+ id: docId,
30
+ ...doc,
31
+ } as CreditLog;
32
+ }
33
+
23
34
  export function useTransactionHistory({
24
35
  config,
25
36
  limit: limitCount = 50,
26
37
  }: UseTransactionHistoryParams): UseTransactionHistoryResult {
27
38
  const userId = useAuthStore(selectUserId);
28
- const [transactions, setTransactions] = useState<CreditLog[]>([]);
29
- const [isLoading, setIsLoading] = useState(true);
30
- const [error, setError] = useState<Error | null>(null);
31
-
32
- useEffect(() => {
33
- if (!userId) {
34
- setTransactions([]);
35
- setIsLoading(false);
36
- setError(null);
37
- return;
38
- }
39
39
 
40
- setIsLoading(true);
41
- setError(null);
40
+ // Build collection query
41
+ const queryRef = useMemo(() => {
42
+ if (!userId) return null;
42
43
 
43
- try {
44
- const db = requireFirestore();
45
- const collectionPath = config.useUserSubcollection
46
- ? `users/${userId}/${config.collectionName}`
47
- : config.collectionName;
44
+ const db = requireFirestore();
45
+ const collectionPath = config.useUserSubcollection
46
+ ? `users/${userId}/${config.collectionName}`
47
+ : config.collectionName;
48
48
 
49
- const q = query(
50
- collection(db, collectionPath),
51
- orderBy("timestamp", "desc"),
52
- limit(limitCount)
53
- ) as Query;
54
-
55
- const unsubscribe = onSnapshot(
56
- q,
57
- (snapshot) => {
58
- const logs: CreditLog[] = [];
59
- snapshot.forEach((doc) => {
60
- logs.push({
61
- id: doc.id,
62
- ...doc.data(),
63
- } as CreditLog);
64
- });
65
- setTransactions(logs);
66
- setIsLoading(false);
67
- },
68
- (err) => {
69
- console.error("[useTransactionHistory] Snapshot error:", err);
70
- setError(err as Error);
71
- setIsLoading(false);
72
- }
73
- );
74
-
75
- return () => unsubscribe();
76
- } catch (err) {
77
- const error = err instanceof Error ? err : new Error(String(err));
78
- console.error("[useTransactionHistory] Setup error:", err);
79
- setError(error);
80
- setIsLoading(false);
81
- }
49
+ return query(
50
+ collection(db, collectionPath),
51
+ orderBy("timestamp", "desc"),
52
+ limit(limitCount)
53
+ );
82
54
  }, [userId, config.collectionName, config.useUserSubcollection, limitCount]);
83
55
 
84
- const refetch = () => {
85
- if (__DEV__) {
86
- console.warn("[useTransactionHistory] Refetch called - not needed for real-time sync");
87
- }
88
- };
56
+ // Use generic real-time sync hook
57
+ const { data, isLoading, error, refetch, isEmpty } = useFirestoreCollectionRealTime(
58
+ userId,
59
+ queryRef,
60
+ mapTransactionLog,
61
+ "useTransactionHistory"
62
+ );
89
63
 
90
64
  return {
91
- transactions,
65
+ transactions: data,
92
66
  isLoading,
93
67
  error,
94
68
  refetch,
95
- isEmpty: transactions.length === 0,
69
+ isEmpty,
96
70
  };
97
71
  }
@@ -35,6 +35,6 @@ export type { PaywallScreenProps } from "./domains/paywall/components/PaywallScr
35
35
 
36
36
  // Root Flow Components
37
37
  export { ManagedSubscriptionFlow } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
38
- export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
38
+ export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow.types";
39
39
  export { SubscriptionFlowStatus } from "./domains/subscription/presentation/useSubscriptionFlow";
40
40
  export { SubscriptionFlowProvider, useSubscriptionFlowStatus } from "./domains/subscription/presentation/providers/SubscriptionFlowProvider";
@@ -36,8 +36,10 @@ class SubscriptionEventBus {
36
36
  const listeners = this.listeners.get(event);
37
37
  if (!listeners || listeners.size === 0) return;
38
38
 
39
- listeners.forEach(callback => {
40
- queueMicrotask(() => {
39
+ // PERFORMANCE: Batch all callbacks in a single microtask to reduce call stack overhead
40
+ // This prevents UI jank when multiple listeners are registered
41
+ queueMicrotask(() => {
42
+ listeners.forEach(callback => {
41
43
  try {
42
44
  callback(data);
43
45
  } catch (error) {