@umituz/react-native-subscription 2.42.0 → 2.43.1

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 (25) 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 +9 -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 +30 -16
  10. package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +49 -17
  11. package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +9 -3
  12. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +4 -8
  13. package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.tsx +4 -4
  14. package/src/domains/subscription/presentation/components/overlay/PurchaseLoadingOverlay.types.ts +2 -0
  15. package/src/domains/subscription/presentation/providers/SubscriptionFlowProvider.tsx +4 -0
  16. package/src/domains/subscription/presentation/usePremium.ts +29 -87
  17. package/src/domains/subscription/presentation/usePremiumActions.ts +94 -0
  18. package/src/domains/subscription/presentation/usePremiumPackages.ts +25 -0
  19. package/src/domains/subscription/presentation/usePremiumStatus.ts +38 -0
  20. package/src/domains/subscription/presentation/useSubscriptionFlow.ts +7 -3
  21. package/src/domains/subscription/presentation/useSyncStatusListener.ts +47 -0
  22. package/src/index.ts +10 -1
  23. package/src/shared/infrastructure/SubscriptionEventBus.ts +11 -1
  24. package/src/domains/credits/application/CreditLimitCalculator.ts +0 -10
  25. 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.42.0",
3
+ "version": "2.43.1",
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;
@@ -2,6 +2,7 @@
2
2
  * Paywall Screen Component
3
3
  *
4
4
  * Full-screen paywall with optimized FlatList for performance and modern design.
5
+ * This is a "dumb" component that receives all data and actions via props.
5
6
  */
6
7
 
7
8
  import React, { useCallback, useEffect, useMemo } from "react";
@@ -14,7 +15,6 @@ import {
14
15
  StatusBar,
15
16
  } from "react-native";
16
17
  import { useNavigation } from "@react-navigation/native";
17
- import { usePremium } from "../../subscription/presentation/usePremium";
18
18
  import { AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
19
19
  import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
20
20
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
@@ -37,13 +37,11 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
37
37
  console.log('[PaywallScreen] 📱 Rendering PaywallScreen', {
38
38
  hasPackages: !!props.packages?.length,
39
39
  packagesCount: props.packages?.length || 0,
40
+ isPremium: props.isPremium,
40
41
  });
41
42
  }
42
43
 
43
- const { purchasePackage, restorePurchase } = usePremium();
44
-
45
44
  const {
46
- onClose,
47
45
  translations,
48
46
  packages = [],
49
47
  features = [],
@@ -52,11 +50,14 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
52
50
  creditAmounts,
53
51
  creditsLabel,
54
52
  heroImage,
53
+ isSyncing,
54
+ onPurchase,
55
+ onRestore,
56
+ onClose,
55
57
  onPurchaseSuccess,
56
58
  onPurchaseError,
57
59
  onAuthRequired,
58
60
  source,
59
- isLoadingPackages
60
61
  } = props;
61
62
 
62
63
  const tokens = useAppDesignTokens();
@@ -80,8 +81,8 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
80
81
  resetState
81
82
  } = usePaywallActions({
82
83
  packages,
83
- purchasePackage,
84
- restorePurchase,
84
+ purchasePackage: onPurchase,
85
+ restorePurchase: onRestore,
85
86
  source,
86
87
  onPurchaseSuccess,
87
88
  onPurchaseError,
@@ -233,7 +234,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
233
234
  return null;
234
235
  }
235
236
 
236
- if (isLoadingPackages) {
237
+ if (isSyncing) {
237
238
  return (
238
239
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary, paddingTop: insets.top }]}>
239
240
  <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(() => ({
@@ -14,8 +14,6 @@ export interface PaywallOrchestratorOptions {
14
14
  heroImage: ImageSourcePropType;
15
15
  isNavReady?: boolean;
16
16
  isLocalizationReady?: boolean;
17
- onAuthRequired?: () => void;
18
- onPurchaseSuccess?: () => void;
19
17
  bestValueIdentifier?: string;
20
18
  creditsLabel?: string;
21
19
  }
@@ -24,6 +22,9 @@ export interface PaywallOrchestratorOptions {
24
22
  * High-level orchestrator for Paywall navigation.
25
23
  * Handles automatic triggers (post-onboarding) and manual triggers (showPaywall state).
26
24
  * Centralizes handlers for success, close, and feedback triggers.
25
+ *
26
+ * This orchestrator fetches all subscription data and passes it to PaywallScreen as props.
27
+ * PaywallScreen is now a "dumb" component that doesn't call usePremium internally.
27
28
  */
28
29
  export function usePaywallOrchestrator({
29
30
  navigation,
@@ -33,17 +34,17 @@ export function usePaywallOrchestrator({
33
34
  heroImage,
34
35
  isNavReady = true,
35
36
  isLocalizationReady = true,
36
- onAuthRequired,
37
- onPurchaseSuccess,
38
37
  bestValueIdentifier = "yearly",
39
38
  creditsLabel,
40
39
  }: PaywallOrchestratorOptions) {
41
- // Sadece isPremium ve packages alıyoruz
42
- // purchasePackage ve restoreRestore fonksiyonlarını ALMIYORUZ
43
- // Çünkü PaywallScreen zaten usePremium hook'unu doğrudan kullanıyor
40
+ // Get all premium data and actions from usePremium
44
41
  const {
45
42
  isPremium,
46
- packages
43
+ packages,
44
+ credits,
45
+ isSyncing,
46
+ purchasePackage,
47
+ restorePurchase,
47
48
  } = usePremium();
48
49
 
49
50
  // Selectors for stable references and fine-grained updates
@@ -59,6 +60,10 @@ export function usePaywallOrchestrator({
59
60
  const { showPaywall, closePaywall } = usePaywallVisibility();
60
61
  const hasNavigatedRef = useRef(false);
61
62
 
63
+ const handleClose = () => {
64
+ closePaywall();
65
+ };
66
+
62
67
  useEffect(() => {
63
68
  if (!isNavReady || !isLocalizationReady) return;
64
69
 
@@ -81,9 +86,9 @@ export function usePaywallOrchestrator({
81
86
  packagesCount: packages.length
82
87
  });
83
88
 
84
- // SADECE DATA geçiyoruz - FONKSİYON YOK
85
- // PaywallScreen kendi usePremium hook'unu kullanacak
89
+ // Pass all data and actions as props - PaywallScreen is now a dumb component
86
90
  navigation.navigate("PaywallScreen", {
91
+ // UI Props
87
92
  translations,
88
93
  legalUrls,
89
94
  features,
@@ -91,9 +96,17 @@ export function usePaywallOrchestrator({
91
96
  creditsLabel,
92
97
  heroImage,
93
98
  source: shouldShowPostOnboarding ? "onboarding" : "manual",
99
+
100
+ // Data Props
94
101
  packages,
95
- onPurchaseSuccess,
96
- onAuthRequired,
102
+ isPremium,
103
+ credits,
104
+ isSyncing,
105
+
106
+ // Action Props
107
+ onPurchase: purchasePackage,
108
+ onRestore: restorePurchase,
109
+ onClose: handleClose,
97
110
  });
98
111
 
99
112
  if (shouldShowPostOnboarding) {
@@ -126,15 +139,16 @@ export function usePaywallOrchestrator({
126
139
  closePaywall,
127
140
  bestValueIdentifier,
128
141
  creditsLabel,
129
- onPurchaseSuccess,
130
- onAuthRequired,
142
+ credits,
143
+ isSyncing,
144
+ purchasePackage,
145
+ restorePurchase,
146
+ handleClose,
131
147
  ]);
132
148
 
133
149
  const completeOnboarding = useSubscriptionFlowStore((state) => state.completeOnboarding);
134
150
 
135
151
  return {
136
- isPremium,
137
- packages,
138
152
  flowState: {
139
153
  isOnboardingComplete,
140
154
  showPostOnboardingPaywall,
@@ -4,7 +4,6 @@ import { getCreditsRepository } from "../../credits/infrastructure/CreditsReposi
4
4
  import { extractRevenueCatData } from "./SubscriptionSyncUtils";
5
5
  import { generatePurchaseId, generateRenewalId } from "./syncIdGenerators";
6
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,
@@ -45,8 +45,6 @@ export interface ManagedSubscriptionFlowProps {
45
45
  heroImage: ImageSourcePropType;
46
46
  bestValueIdentifier?: string;
47
47
  creditsLabel?: string;
48
- onAuthRequired?: () => void;
49
- onPurchaseSuccess?: () => void;
50
48
  };
51
49
 
52
50
  // Feedback Configuration
@@ -55,7 +53,7 @@ export interface ManagedSubscriptionFlowProps {
55
53
  onSubmit?: (data: { reason: string; otherText?: string }) => void | Promise<void>;
56
54
  };
57
55
 
58
- // Offline Configuration
56
+ // Offline Configuration (optional)
59
57
  offline?: {
60
58
  isOffline: boolean;
61
59
  message: string;
@@ -104,10 +102,10 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
104
102
  }
105
103
  }, [isSplashComplete, islocalizationReady]);
106
104
 
107
- const {
108
- flowState,
105
+ const {
106
+ flowState,
109
107
  setShowFeedback,
110
- completeOnboarding
108
+ completeOnboarding
111
109
  } = usePaywallOrchestrator({
112
110
  navigation,
113
111
  isNavReady,
@@ -116,8 +114,6 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
116
114
  features: paywall.features,
117
115
  legalUrls: paywall.legalUrls,
118
116
  heroImage: paywall.heroImage,
119
- onAuthRequired: paywall.onAuthRequired,
120
- onPurchaseSuccess: paywall.onPurchaseSuccess,
121
117
  bestValueIdentifier: paywall.bestValueIdentifier,
122
118
  creditsLabel: paywall.creditsLabel,
123
119
  });
@@ -2,24 +2,24 @@
2
2
  * Purchase Loading Overlay
3
3
  * Full-screen overlay shown during purchase operations
4
4
  * Locks the UI and shows a spinner with optional message
5
+ *
6
+ * This is now a props-based component. Pass isLoading from parent component.
5
7
  */
6
8
 
7
9
  import React from "react";
8
10
  import { View, Modal, StyleSheet } from "react-native";
9
11
  import { AtomicSpinner, AtomicText } from "@umituz/react-native-design-system/atoms";
10
12
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
- import { usePurchaseLoadingStore, selectIsPurchasing } from "../../stores/purchaseLoadingStore";
12
13
  import type { PurchaseLoadingOverlayProps } from "./PurchaseLoadingOverlay.types";
13
14
 
14
15
  export type { PurchaseLoadingOverlayProps };
15
16
 
16
17
  export const PurchaseLoadingOverlay: React.FC<PurchaseLoadingOverlayProps> = React.memo(
17
- ({ loadingText }) => {
18
+ ({ loadingText, isLoading }) => {
18
19
  const tokens = useAppDesignTokens();
19
- const isPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
20
20
 
21
21
  return (
22
- <Modal visible={isPurchasing} transparent animationType="none" statusBarTranslucent>
22
+ <Modal visible={isLoading} transparent animationType="none" statusBarTranslucent>
23
23
  <View style={[styles.container, { backgroundColor: "rgba(0, 0, 0, 0.7)" }]}>
24
24
  <View style={[styles.content, { backgroundColor: tokens.colors.surface }]}>
25
25
  <AtomicSpinner size="lg" color="primary" />
@@ -1,4 +1,6 @@
1
1
  export interface PurchaseLoadingOverlayProps {
2
+ /** Whether the overlay is visible */
3
+ isLoading: boolean;
2
4
  /** Loading message to display */
3
5
  loadingText?: string;
4
6
  }
@@ -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,38 @@
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
+ * This provides backward compatibility with existing code while allowing
11
+ * components to use more focused hooks (usePremiumStatus, usePremiumPackages, usePremiumActions)
12
+ * for better performance and testability.
13
+ *
14
+ * For new components, consider using the focused hooks:
15
+ * - usePremiumStatus() - when you only need premium status
16
+ * - usePremiumPackages() - when you only need package data
17
+ * - usePremiumActions() - when you only need actions
18
+ *
19
+ * This facade re-renders when ANY of the sub-hooks change, whereas focused hooks
20
+ * only re-render when their specific data changes.
21
+ */
16
22
  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]);
23
+ const status = usePremiumStatus();
24
+ const packages = usePremiumPackages();
25
+ const actions = usePremiumActions();
61
26
 
62
27
  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,
28
+ ...status,
29
+ ...packages,
30
+ ...actions,
31
+ // Merge loading states for backward compatibility
32
+ isLoading: status.isSyncing || packages.isLoading || actions.isLoading,
79
33
  }), [
80
- isPremium,
81
- statusLoading,
82
- creditsLoading,
83
- packagesLoading,
84
- purchaseMutation.isPending,
85
- restoreMutation.isPending,
34
+ status,
86
35
  packages,
87
- credits,
88
- showPaywall,
89
- isSyncing,
90
- handlePurchase,
91
- handleRestore,
92
- setShowPaywall,
93
- closePaywall,
94
- openPaywall,
36
+ actions,
95
37
  ]);
96
38
  };
@@ -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
@@ -19,7 +19,7 @@ export type {
19
19
  PurchaseType,
20
20
  } from "./domains/subscription/core/SubscriptionConstants";
21
21
  export type { SubscriptionMetadata } from "./domains/subscription/core/types/SubscriptionMetadata";
22
- export type { PremiumStatus } from "./domains/subscription/core/types/PremiumStatus";
22
+ export type { PremiumStatus as PremiumStatusMetadata } from "./domains/subscription/core/types/PremiumStatus";
23
23
  export type { CreditInfo } from "./domains/subscription/core/types/CreditInfo";
24
24
  export {
25
25
  createDefaultSubscriptionStatus,
@@ -63,10 +63,18 @@ export { useDeductCredit } from "./domains/credits/presentation/deduct-credit/us
63
63
  export { useFeatureGate } from "./domains/subscription/presentation/useFeatureGate";
64
64
  export { usePaywallVisibility, paywallControl } from "./domains/subscription/presentation/usePaywallVisibility";
65
65
  export { usePremium } from "./domains/subscription/presentation/usePremium";
66
+ export { usePremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
67
+ export { usePremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
68
+ export { usePremiumActions } from "./domains/subscription/presentation/usePremiumActions";
69
+ export type { UsePremiumResult } from "./domains/subscription/presentation/usePremium.types";
70
+ export type { PremiumStatus } from "./domains/subscription/presentation/usePremiumStatus";
71
+ export type { PremiumPackages } from "./domains/subscription/presentation/usePremiumPackages";
72
+ export type { PremiumActions } from "./domains/subscription/presentation/usePremiumActions";
66
73
  export { useSubscriptionFlowStore } from "./domains/subscription/presentation/useSubscriptionFlow";
67
74
  export type { SubscriptionFlowState, SubscriptionFlowActions, SubscriptionFlowStore } from "./domains/subscription/presentation/useSubscriptionFlow";
68
75
  export { useSubscriptionStatus } from "./domains/subscription/presentation/useSubscriptionStatus";
69
76
  export type { SubscriptionStatusResult } from "./domains/subscription/presentation/useSubscriptionStatus.types";
77
+ export { useSyncStatusListener } from "./domains/subscription/presentation/useSyncStatusListener";
70
78
  export { usePaywallFeedback } from "./presentation/hooks/feedback/usePaywallFeedback";
71
79
  export {
72
80
  usePaywallFeedbackSubmit,
@@ -108,6 +116,7 @@ export type {
108
116
  CreditsResult,
109
117
  DeductCreditsResult,
110
118
  } from "./domains/credits/core/Credits";
119
+ export { CreditLimitService, calculateCreditLimit } from "./domains/credits/domain/services/CreditLimitService";
111
120
 
112
121
  // Utils
113
122
  export {
@@ -76,4 +76,14 @@ export const SUBSCRIPTION_EVENTS = {
76
76
  PURCHASE_COMPLETED: "purchase_completed",
77
77
  RENEWAL_DETECTED: "renewal_detected",
78
78
  PREMIUM_STATUS_CHANGED: "premium_status_changed",
79
- };
79
+ SYNC_STATUS_CHANGED: "sync_status_changed",
80
+ } as const;
81
+
82
+ export const FLOW_EVENTS = {
83
+ ONBOARDING_COMPLETED: "flow_onboarding_completed",
84
+ PAYWALL_SHOWN: "flow_paywall_shown",
85
+ PAYWALL_CLOSED: "flow_paywall_closed",
86
+ } as const;
87
+
88
+ export type SubscriptionEventType = typeof SUBSCRIPTION_EVENTS[keyof typeof SUBSCRIPTION_EVENTS];
89
+ export type FlowEventType = typeof FLOW_EVENTS[keyof typeof FLOW_EVENTS];
@@ -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,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);