@umituz/react-native-subscription 3.1.10 → 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 (39) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/presentation/useCreditsRealTime.ts +10 -5
  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/screens/components/SubscriptionHeaderContent.tsx +119 -103
  27. package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
  28. package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
  29. package/src/index.components.ts +1 -1
  30. package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
  31. package/src/shared/presentation/hooks/useFirestoreRealTime.ts +22 -6
  32. package/src/shared/utils/errors/errorAssertions.ts +35 -0
  33. package/src/shared/utils/errors/errorConversion.ts +73 -0
  34. package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
  35. package/src/shared/utils/errors/errorWrappers.ts +54 -0
  36. package/src/shared/utils/errors/index.ts +19 -0
  37. package/src/shared/utils/errors/serviceErrors.ts +36 -0
  38. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
  39. package/src/shared/utils/errorUtils.ts +0 -195
@@ -1,240 +1,22 @@
1
- import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
2
- import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
3
- import type { InitializerDeps } from "./RevenueCatInitializer.types";
4
- import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
5
- import { UserSwitchMutex } from "./UserSwitchMutex";
6
- import { getPremiumEntitlement } from "../../core/types/RevenueCatTypes";
7
- import { ANONYMOUS_CACHE_KEY, type PeriodType } from "../../../subscription/core/SubscriptionConstants";
8
-
9
- declare const __DEV__: boolean;
10
-
11
- function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings | null): InitializeResult {
12
- const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
13
- return { success: true, offering: offerings?.current ?? null, isPremium };
14
- }
15
-
16
1
  /**
17
- * Fetch offerings separately - non-fatal if it fails.
18
- * Empty offerings (no products configured in RevenueCat dashboard) should NOT
19
- * block SDK initialization. The SDK is still usable for premium checks, purchases, etc.
2
+ * User Switch Handler
3
+ *
4
+ * Main entry point for user switch operations.
5
+ * Exports functions from split modules for better organization.
20
6
  */
21
- async function fetchOfferingsSafe(): Promise<PurchasesOfferings | null> {
22
- try {
23
- return await Purchases.getOfferings();
24
- } catch (error) {
25
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
26
- console.warn('[UserSwitchHandler] Offerings fetch failed (non-fatal):', error);
27
- }
28
- return null;
29
- }
30
- }
31
-
32
- function normalizeUserId(userId: string): string | null {
33
- return (userId && userId.length > 0 && userId !== ANONYMOUS_CACHE_KEY) ? userId : null;
34
- }
35
-
36
- function isAnonymousId(userId: string): boolean {
37
- return userId.startsWith('$RCAnonymous') || userId.startsWith('device_');
38
- }
39
-
40
- export async function handleUserSwitch(
41
- deps: InitializerDeps,
42
- userId: string
43
- ): Promise<InitializeResult> {
44
- const mutexKey = userId || ANONYMOUS_CACHE_KEY;
45
-
46
- // Acquire mutex to prevent concurrent Purchases.logIn() calls
47
- const { shouldProceed, existingPromise } = await UserSwitchMutex.acquire(mutexKey);
48
-
49
- if (!shouldProceed && existingPromise) {
50
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
51
- console.log('[UserSwitchHandler] Using result from active switch operation');
52
- }
53
- return existingPromise as Promise<InitializeResult>;
54
- }
55
-
56
- const switchOperation = performUserSwitch(deps, userId);
57
- UserSwitchMutex.setPromise(switchOperation);
58
- return switchOperation;
59
- }
60
-
61
- async function performUserSwitch(
62
- deps: InitializerDeps,
63
- userId: string
64
- ): Promise<InitializeResult> {
65
- try {
66
- const currentAppUserId = await Purchases.getAppUserID();
67
- const normalizedUserId = normalizeUserId(userId);
68
- const normalizedCurrentUserId = isAnonymousId(currentAppUserId) ? null : currentAppUserId;
69
-
70
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
71
- console.log('[UserSwitchHandler] handleUserSwitch:', {
72
- providedUserId: userId,
73
- normalizedUserId: normalizedUserId || '(null - anonymous)',
74
- currentAppUserId,
75
- normalizedCurrentUserId: normalizedCurrentUserId || '(null - anonymous)',
76
- needsSwitch: normalizedCurrentUserId !== normalizedUserId,
77
- });
78
- }
79
-
80
- let customerInfo;
81
-
82
- if (normalizedCurrentUserId !== normalizedUserId) {
83
- if (normalizedUserId) {
84
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
85
- console.log('[UserSwitchHandler] Calling Purchases.logIn() to switch from anonymous to:', normalizedUserId);
86
- }
87
- const result = await Purchases.logIn(normalizedUserId!);
88
- customerInfo = result.customerInfo;
89
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
90
- console.log('[UserSwitchHandler] Purchases.logIn() successful, created:', result.created);
91
- }
92
- } else {
93
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
94
- console.log('[UserSwitchHandler] User is anonymous, fetching customer info');
95
- }
96
- customerInfo = await Purchases.getCustomerInfo();
97
- }
98
- } else {
99
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
100
- console.log('[UserSwitchHandler] No user switch needed, fetching current customer info');
101
- }
102
- customerInfo = await Purchases.getCustomerInfo();
103
- }
104
-
105
- deps.setInitialized(true);
106
- deps.setCurrentUserId(normalizedUserId || undefined);
107
- const offerings = await fetchOfferingsSafe();
108
-
109
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
110
- console.log('[UserSwitchHandler] User switch completed successfully');
111
- }
112
-
113
- return buildSuccessResult(deps, customerInfo, offerings);
114
- } catch (error) {
115
- let currentAppUserId = 'unknown';
116
- try {
117
- currentAppUserId = await Purchases.getAppUserID();
118
- } catch {
119
- // Ignore error in error handler
120
- }
121
-
122
- console.error('[UserSwitchHandler] Failed during user switch or fetch', {
123
- userId,
124
- currentAppUserId,
125
- error
126
- });
127
- return FAILED_INITIALIZATION_RESULT;
128
- }
129
- }
130
-
131
- export async function handleInitialConfiguration(
132
- deps: InitializerDeps,
133
- userId: string,
134
- apiKey: string
135
- ): Promise<InitializeResult> {
136
- try {
137
- const normalizedUserId = normalizeUserId(userId);
138
-
139
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
140
- console.log('[UserSwitchHandler] handleInitialConfiguration:', {
141
- providedUserId: userId,
142
- normalizedUserId: normalizedUserId || '(null - anonymous)',
143
- apiKeyPrefix: apiKey.substring(0, 5) + '...',
144
- isTestKey: apiKey.startsWith('test_'),
145
- });
146
- }
147
-
148
- Purchases.setLogLevel(
149
- (typeof __DEV__ !== 'undefined' && __DEV__)
150
- ? Purchases.LOG_LEVEL.INFO
151
- : Purchases.LOG_LEVEL.ERROR
152
- );
153
-
154
- await Purchases.configure({ apiKey, appUserID: normalizedUserId || undefined });
155
- deps.setInitialized(true);
156
- deps.setCurrentUserId(normalizedUserId || undefined);
157
-
158
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
159
- console.log('[UserSwitchHandler] Purchases.configure() successful');
160
- }
161
-
162
- // Fetch customer info (critical) and offerings (non-fatal) separately.
163
- // Empty offerings should NOT block initialization - SDK is still usable.
164
- const [customerInfo, offerings] = await Promise.all([
165
- Purchases.getCustomerInfo(),
166
- fetchOfferingsSafe(),
167
- ]);
168
-
169
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
170
- const currentUserId = await Purchases.getAppUserID();
171
- console.log('[UserSwitchHandler] Initial configuration completed:', {
172
- revenueCatUserId: currentUserId,
173
- activeEntitlements: Object.keys(customerInfo.entitlements.active),
174
- offeringsCount: offerings?.all ? Object.keys(offerings.all).length : 0,
175
- });
176
- }
177
-
178
- // Sync premium status via callback (if configured).
179
- // Only when we have a real Firebase UID — skip for pre-auth anonymous state
180
- // to avoid writing to Firestore with a RevenueCat-generated ID that doesn't
181
- // match request.auth.uid.
182
- if (deps.config.onPremiumStatusChanged && normalizedUserId) {
183
- try {
184
- const premiumEntitlement = getPremiumEntitlement(
185
- customerInfo,
186
- deps.config.entitlementIdentifier
187
- );
188
-
189
- if (premiumEntitlement) {
190
- const subscription = customerInfo.subscriptionsByProductIdentifier?.[premiumEntitlement.productIdentifier];
191
-
192
- await deps.config.onPremiumStatusChanged({
193
- userId: normalizedUserId,
194
- isPremium: true,
195
- productId: premiumEntitlement.productIdentifier,
196
- expirationDate: premiumEntitlement.expirationDate ?? null,
197
- willRenew: premiumEntitlement.willRenew,
198
- periodType: premiumEntitlement.periodType as PeriodType | undefined,
199
- storeTransactionId: subscription?.storeTransactionId ?? undefined,
200
- unsubscribeDetectedAt: premiumEntitlement.unsubscribeDetectedAt ?? null,
201
- billingIssueDetectedAt: premiumEntitlement.billingIssueDetectedAt ?? null,
202
- store: premiumEntitlement.store ?? null,
203
- ownershipType: premiumEntitlement.ownershipType ?? null,
204
- });
205
- } else {
206
- await deps.config.onPremiumStatusChanged({
207
- userId: normalizedUserId,
208
- isPremium: false,
209
- });
210
- }
211
- } catch (error) {
212
- // Log error but don't fail initialization
213
- console.error('[UserSwitchHandler] Premium status sync callback failed:', error);
214
- }
215
- }
216
-
217
- return buildSuccessResult(deps, customerInfo, offerings);
218
- } catch (error) {
219
- console.error('[UserSwitchHandler] SDK configuration failed', {
220
- userId,
221
- error
222
- });
223
- return FAILED_INITIALIZATION_RESULT;
224
- }
225
- }
226
7
 
227
- export async function fetchCurrentUserData(deps: InitializerDeps): Promise<InitializeResult> {
228
- try {
229
- const [customerInfo, offerings] = await Promise.all([
230
- Purchases.getCustomerInfo(),
231
- fetchOfferingsSafe(),
232
- ]);
233
- return buildSuccessResult(deps, customerInfo, offerings);
234
- } catch (error) {
235
- console.error('[UserSwitchHandler] Failed to fetch customer info for initialized user', {
236
- error
237
- });
238
- return FAILED_INITIALIZATION_RESULT;
239
- }
240
- }
8
+ export {
9
+ normalizeUserId,
10
+ isAnonymousId,
11
+ buildSuccessResult,
12
+ fetchOfferingsSafe,
13
+ } from './userSwitchHelpers';
14
+
15
+ export {
16
+ handleUserSwitch,
17
+ } from './userSwitchCore';
18
+
19
+ export {
20
+ handleInitialConfiguration,
21
+ fetchCurrentUserData,
22
+ } from './userSwitchInitializer';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * User Switch Helper Functions
3
+ *
4
+ * Utility functions for user switch operations.
5
+ */
6
+
7
+ import Purchases, { type CustomerInfo, type PurchasesOfferings } from "react-native-purchases";
8
+ import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
9
+ import type { InitializerDeps } from "./RevenueCatInitializer.types";
10
+ import { ANONYMOUS_CACHE_KEY } from "../../../subscription/core/SubscriptionConstants";
11
+
12
+ declare const __DEV__: boolean;
13
+
14
+ /**
15
+ * Normalize user ID to null if empty or anonymous cache key.
16
+ */
17
+ export function normalizeUserId(userId: string): string | null {
18
+ return (userId && userId.length > 0 && userId !== ANONYMOUS_CACHE_KEY) ? userId : null;
19
+ }
20
+
21
+ /**
22
+ * Check if the given user ID is an anonymous RevenueCat ID.
23
+ */
24
+ export function isAnonymousId(userId: string): boolean {
25
+ return userId.startsWith('$RCAnonymous') || userId.startsWith('device_');
26
+ }
27
+
28
+ /**
29
+ * Build successful initialization result.
30
+ */
31
+ export function buildSuccessResult(
32
+ deps: InitializerDeps,
33
+ customerInfo: CustomerInfo,
34
+ offerings: PurchasesOfferings | null
35
+ ): InitializeResult {
36
+ const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
37
+ return { success: true, offering: offerings?.current ?? null, isPremium };
38
+ }
39
+
40
+ /**
41
+ * Fetch offerings separately - non-fatal if it fails.
42
+ *
43
+ * Empty offerings (no products configured in RevenueCat dashboard) should NOT
44
+ * block SDK initialization. The SDK is still usable for premium checks, purchases, etc.
45
+ */
46
+ export async function fetchOfferingsSafe(): Promise<PurchasesOfferings | null> {
47
+ try {
48
+ return await Purchases.getOfferings();
49
+ } catch (error) {
50
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
51
+ console.warn('[UserSwitchHelpers] Offerings fetch failed (non-fatal):', error);
52
+ }
53
+ return null;
54
+ }
55
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * User Switch Initializer
3
+ *
4
+ * Handles initial RevenueCat SDK configuration and user data fetching.
5
+ */
6
+
7
+ import Purchases, { type CustomerInfo } from "react-native-purchases";
8
+ import type { InitializeResult } from "../../../../shared/application/ports/IRevenueCatService";
9
+ import type { InitializerDeps } from "./RevenueCatInitializer.types";
10
+ import { FAILED_INITIALIZATION_RESULT } from "./initializerConstants";
11
+ import { getPremiumEntitlement } from "../../core/types/RevenueCatTypes";
12
+ import type { PeriodType } from "../../../subscription/core/SubscriptionConstants";
13
+ import {
14
+ normalizeUserId,
15
+ buildSuccessResult,
16
+ fetchOfferingsSafe,
17
+ } from "./userSwitchHelpers";
18
+
19
+ declare const __DEV__: boolean;
20
+
21
+ /**
22
+ * Handle initial SDK configuration with API key and user ID.
23
+ */
24
+ export async function handleInitialConfiguration(
25
+ deps: InitializerDeps,
26
+ userId: string,
27
+ apiKey: string
28
+ ): Promise<InitializeResult> {
29
+ try {
30
+ const normalizedUserId = normalizeUserId(userId);
31
+
32
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
33
+ console.log('[UserSwitchInitializer] handleInitialConfiguration:', {
34
+ providedUserId: userId,
35
+ normalizedUserId: normalizedUserId || '(null - anonymous)',
36
+ apiKeyPrefix: apiKey.substring(0, 5) + '...',
37
+ isTestKey: apiKey.startsWith('test_'),
38
+ });
39
+ }
40
+
41
+ Purchases.setLogLevel(
42
+ (typeof __DEV__ !== 'undefined' && __DEV__)
43
+ ? Purchases.LOG_LEVEL.INFO
44
+ : Purchases.LOG_LEVEL.ERROR
45
+ );
46
+
47
+ await Purchases.configure({ apiKey, appUserID: normalizedUserId || undefined });
48
+ deps.setInitialized(true);
49
+ deps.setCurrentUserId(normalizedUserId || undefined);
50
+
51
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
52
+ console.log('[UserSwitchInitializer] Purchases.configure() successful');
53
+ }
54
+
55
+ const [customerInfo, offerings] = await Promise.all([
56
+ Purchases.getCustomerInfo(),
57
+ fetchOfferingsSafe(),
58
+ ]);
59
+
60
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
61
+ const currentUserId = await Purchases.getAppUserID();
62
+ console.log('[UserSwitchInitializer] Initial configuration completed:', {
63
+ revenueCatUserId: currentUserId,
64
+ activeEntitlements: Object.keys(customerInfo.entitlements.active),
65
+ offeringsCount: offerings?.all ? Object.keys(offerings.all).length : 0,
66
+ });
67
+ }
68
+
69
+ await syncPremiumStatusIfConfigured(deps, normalizedUserId, customerInfo);
70
+
71
+ return buildSuccessResult(deps, customerInfo, offerings);
72
+ } catch (error) {
73
+ console.error('[UserSwitchInitializer] SDK configuration failed', {
74
+ userId,
75
+ error
76
+ });
77
+ return FAILED_INITIALIZATION_RESULT;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Fetch current user data without switching users.
83
+ */
84
+ export async function fetchCurrentUserData(deps: InitializerDeps): Promise<InitializeResult> {
85
+ try {
86
+ const [customerInfo, offerings] = await Promise.all([
87
+ Purchases.getCustomerInfo(),
88
+ fetchOfferingsSafe(),
89
+ ]);
90
+ return buildSuccessResult(deps, customerInfo, offerings);
91
+ } catch (error) {
92
+ console.error('[UserSwitchInitializer] Failed to fetch customer info for initialized user', {
93
+ error
94
+ });
95
+ return FAILED_INITIALIZATION_RESULT;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Sync premium status to callback if configured.
101
+ * Only when we have a real Firebase UID — skip for pre-auth anonymous state.
102
+ */
103
+ async function syncPremiumStatusIfConfigured(
104
+ deps: InitializerDeps,
105
+ normalizedUserId: string | null,
106
+ customerInfo: CustomerInfo
107
+ ): Promise<void> {
108
+ if (!deps.config.onPremiumStatusChanged || !normalizedUserId) {
109
+ return;
110
+ }
111
+
112
+ try {
113
+ const premiumEntitlement = getPremiumEntitlement(
114
+ customerInfo,
115
+ deps.config.entitlementIdentifier
116
+ );
117
+
118
+ if (premiumEntitlement) {
119
+ const subscription = customerInfo.subscriptionsByProductIdentifier?.[premiumEntitlement.productIdentifier];
120
+
121
+ await deps.config.onPremiumStatusChanged({
122
+ userId: normalizedUserId,
123
+ isPremium: true,
124
+ productId: premiumEntitlement.productIdentifier,
125
+ expirationDate: premiumEntitlement.expirationDate ?? null,
126
+ willRenew: premiumEntitlement.willRenew,
127
+ periodType: premiumEntitlement.periodType as PeriodType | undefined,
128
+ storeTransactionId: subscription?.storeTransactionId ?? undefined,
129
+ unsubscribeDetectedAt: premiumEntitlement.unsubscribeDetectedAt ?? null,
130
+ billingIssueDetectedAt: premiumEntitlement.billingIssueDetectedAt ?? null,
131
+ store: premiumEntitlement.store ?? null,
132
+ ownershipType: premiumEntitlement.ownershipType ?? null,
133
+ });
134
+ } else {
135
+ await deps.config.onPremiumStatusChanged({
136
+ userId: normalizedUserId,
137
+ isPremium: false,
138
+ });
139
+ }
140
+ } catch (error) {
141
+ console.error('[UserSwitchInitializer] Premium status sync callback failed:', error);
142
+ }
143
+ }
@@ -29,6 +29,9 @@ class SubscriptionManagerImpl {
29
29
 
30
30
  private ensurePackageHandlerInitialized(): void {
31
31
  if (this.packageHandler) return;
32
+ if (!this.serviceInstance || !this.managerConfig) {
33
+ throw new Error('[SubscriptionManager] Cannot create package handler without service and config');
34
+ }
32
35
  this.packageHandler = createPackageHandler(this.serviceInstance, this.managerConfig);
33
36
  }
34
37
 
@@ -70,7 +73,7 @@ class SubscriptionManagerImpl {
70
73
  });
71
74
  }
72
75
 
73
- const { service, success } = await performServiceInitialization(this.managerConfig!.config, userId);
76
+ const { service, success } = await performServiceInitialization(this.managerConfig.config, userId);
74
77
  this.serviceInstance = service ?? null;
75
78
  this.ensurePackageHandlerInitialized();
76
79
 
@@ -128,7 +131,7 @@ class SubscriptionManagerImpl {
128
131
  this.ensureConfigured();
129
132
  ensureServiceAvailable(this.serviceInstance);
130
133
  this.ensurePackageHandlerInitialized();
131
- return checkPremiumStatusFromService(this.serviceInstance!, this.packageHandler!);
134
+ return checkPremiumStatusFromService(this.serviceInstance, this.packageHandler!);
132
135
  }
133
136
 
134
137
  async reset(): Promise<void> {
@@ -145,7 +148,7 @@ class SubscriptionManagerImpl {
145
148
 
146
149
  getEntitlementId(): string {
147
150
  this.ensureConfigured();
148
- return this.managerConfig!.config.entitlementIdentifier;
151
+ return this.managerConfig.config.entitlementIdentifier;
149
152
  }
150
153
  }
151
154
 
@@ -9,6 +9,6 @@ export const performServiceInitialization = async (config: RevenueCatConfig, use
9
9
 
10
10
  ensureServiceAvailable(service);
11
11
 
12
- const result = await service!.initialize(userId);
13
- return { service: service!, success: result.success };
12
+ const result = await service.initialize(userId);
13
+ return { service, success: result.success };
14
14
  };
@@ -11,7 +11,7 @@ export const createPackageHandler = (
11
11
  ensureConfigured(config);
12
12
 
13
13
  return new PackageHandler(
14
- service!,
15
- config!.config.entitlementIdentifier
14
+ service,
15
+ config.config.entitlementIdentifier
16
16
  );
17
17
  };
@@ -3,7 +3,7 @@ import type { IRevenueCatService } from "../../../../shared/application/ports/IR
3
3
  import { getRevenueCatService } from "../services/revenueCatServiceInstance";
4
4
  import type { InitializationCache } from "../utils/InitializationCache";
5
5
 
6
- export function ensureConfigured(config: SubscriptionManagerConfig | null): void {
6
+ export function ensureConfigured(config: SubscriptionManagerConfig | null): asserts config is SubscriptionManagerConfig {
7
7
  if (!config) {
8
8
  throw new Error("SubscriptionManager not configured");
9
9
  }
@@ -33,7 +33,7 @@ export function getOrCreateService(
33
33
  return serviceInstance;
34
34
  }
35
35
 
36
- export function ensureServiceAvailable(service: IRevenueCatService | null): void {
36
+ export function ensureServiceAvailable(service: IRevenueCatService | null): asserts service is IRevenueCatService {
37
37
  if (!service) {
38
38
  throw new Error("Service instance not available");
39
39
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ManagedSubscriptionFlow - State Transition Logic
3
+ *
4
+ * Extracted state transition useEffect hooks for better separation.
5
+ */
6
+
7
+ import { useEffect } from "react";
8
+ import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
9
+
10
+ interface UseStateTransitionsParams {
11
+ status: SubscriptionFlowStatus;
12
+ isPremium: boolean;
13
+ isSyncing: boolean;
14
+ showFeedback: boolean;
15
+ }
16
+
17
+ /**
18
+ * Hook containing all state transition logic.
19
+ * Extracted for better testability and separation of concerns.
20
+ */
21
+ export function useStateTransitions({
22
+ status,
23
+ isPremium,
24
+ isSyncing,
25
+ showFeedback,
26
+ }: UseStateTransitionsParams) {
27
+ const completePaywall = useSubscriptionFlowStore((s) => s.completePaywall);
28
+ const showPaywall = useSubscriptionFlowStore((s) => s.showPaywall);
29
+ const showFeedbackScreen = useSubscriptionFlowStore((s) => s.showFeedbackScreen);
30
+
31
+ // Transition from CHECK_PREMIUM to appropriate state
32
+ useEffect(() => {
33
+ if (status === SubscriptionFlowStatus.CHECK_PREMIUM && !isSyncing) {
34
+ const paywallShown = useSubscriptionFlowStore.getState().paywallShown;
35
+
36
+ if (isPremium) {
37
+ completePaywall(true);
38
+ } else if (!paywallShown) {
39
+ showPaywall();
40
+ } else {
41
+ completePaywall(false);
42
+ }
43
+ }
44
+ }, [status, isPremium, isSyncing, showPaywall, completePaywall]);
45
+
46
+ // Show feedback screen when ready
47
+ useEffect(() => {
48
+ if (status === SubscriptionFlowStatus.READY && showFeedback) {
49
+ showFeedbackScreen();
50
+ }
51
+ }, [status, showFeedback, showFeedbackScreen]);
52
+ }