@umituz/react-native-subscription 2.27.92 → 2.27.94

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/application/CreditsInitializer.ts +99 -40
  3. package/src/domains/credits/application/DeductCreditsCommand.ts +31 -13
  4. package/src/domains/credits/application/PurchaseMetadataGenerator.ts +17 -23
  5. package/src/domains/credits/core/Credits.ts +39 -39
  6. package/src/domains/credits/core/CreditsMapper.ts +11 -10
  7. package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
  8. package/src/domains/credits/infrastructure/CreditsRepository.ts +46 -59
  9. package/src/domains/paywall/components/PaywallModal.tsx +1 -1
  10. package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
  11. package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +20 -20
  12. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
  13. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
  14. package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
  15. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
  16. package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
  17. package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
  18. package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
  19. package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
  20. package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
  21. package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
  22. package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
  23. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
  24. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
  25. package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
  26. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
  27. package/src/shared/application/ports/IRevenueCatService.ts +2 -0
  28. package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
  29. package/src/presentation/README.md +0 -125
  30. package/src/presentation/hooks/README.md +0 -156
  31. package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
  32. package/src/presentation/hooks/useCredits.md +0 -103
  33. package/src/presentation/hooks/useDeductCredit.md +0 -100
  34. package/src/presentation/hooks/useFeatureGate.md +0 -112
  35. package/src/presentation/hooks/usePaywall.md +0 -89
  36. package/src/presentation/hooks/usePaywallOperations.md +0 -92
  37. package/src/presentation/hooks/usePaywallVisibility.md +0 -95
  38. package/src/presentation/hooks/usePremium.md +0 -88
  39. package/src/presentation/hooks/useSubscriptionSettingsConfig.md +0 -94
@@ -3,7 +3,7 @@
3
3
  * Optimized to use Design Patterns: Command, Observer, and Strategy.
4
4
  */
5
5
 
6
- import { doc, getDoc, serverTimestamp, updateDoc, type Firestore } from "firebase/firestore";
6
+ import { doc, getDoc, type Firestore } from "firebase/firestore";
7
7
  import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
8
8
  import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
9
9
  import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
@@ -16,8 +16,8 @@ import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
16
16
  export class CreditsRepository extends BaseRepository {
17
17
  private deductCommand: DeductCreditsCommand;
18
18
 
19
- constructor(private config: CreditsConfig) {
20
- super();
19
+ constructor(private config: CreditsConfig) {
20
+ super();
21
21
  this.deductCommand = new DeductCreditsCommand((db, uid) => this.getRef(db, uid));
22
22
  }
23
23
 
@@ -29,74 +29,61 @@ export class CreditsRepository extends BaseRepository {
29
29
 
30
30
  async getCredits(userId: string): Promise<CreditsResult> {
31
31
  const db = getFirestore();
32
- if (!db) return { success: false, error: { message: "No DB", code: "DB_ERR" } };
33
- try {
34
- const snap = await getDoc(this.getRef(db, userId));
35
- if (!snap.exists()) return { success: true, data: undefined };
36
-
37
- const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
38
- return { success: true, data: entity };
39
- } catch (e: unknown) {
40
- const message = e instanceof Error ? e.message : String(e);
41
- return { success: false, error: { message, code: "FETCH_ERR" } };
32
+ if (!db) {
33
+ throw new Error("Firestore instance is not available");
42
34
  }
35
+
36
+ const snap = await getDoc(this.getRef(db, userId));
37
+ if (!snap.exists()) {
38
+ return { success: true, data: null, error: null };
39
+ }
40
+
41
+ const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
42
+ return { success: true, data: entity, error: null };
43
43
  }
44
44
 
45
45
  async initializeCredits(
46
- userId: string, purchaseId?: string, productId?: string,
47
- source?: PurchaseSource, revenueCatData?: RevenueCatData
46
+ userId: string,
47
+ purchaseId: string,
48
+ productId: string,
49
+ source: PurchaseSource,
50
+ revenueCatData: RevenueCatData
48
51
  ): Promise<CreditsResult> {
49
52
  const db = getFirestore();
50
- if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
51
- try {
52
- // Use CreditLimitCalculator (Refactoring Logic)
53
- const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
54
- const cfg = { ...this.config, creditLimit };
55
-
56
- const result = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, {
57
- productId, source,
58
- expirationDate: revenueCatData?.expirationDate,
59
- willRenew: revenueCatData?.willRenew,
60
- originalTransactionId: revenueCatData?.originalTransactionId,
61
- isPremium: revenueCatData?.isPremium,
62
- periodType: revenueCatData?.periodType,
63
- });
64
-
65
- return {
66
- success: true,
67
- data: result.finalData ? CreditsMapper.toEntity(result.finalData) : undefined,
68
- };
69
- } catch (e: unknown) {
70
- const message = e instanceof Error ? e.message : String(e);
71
- return { success: false, error: { message, code: "INIT_ERR" } };
53
+ if (!db) {
54
+ throw new Error("Firestore instance is not available");
72
55
  }
56
+
57
+ const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
58
+ const cfg = { ...this.config, creditLimit };
59
+
60
+ const result = await initializeCreditsTransaction(
61
+ db,
62
+ this.getRef(db, userId),
63
+ cfg,
64
+ purchaseId,
65
+ {
66
+ productId,
67
+ source,
68
+ expirationDate: revenueCatData.expirationDate,
69
+ willRenew: revenueCatData.willRenew,
70
+ originalTransactionId: revenueCatData.originalTransactionId,
71
+ isPremium: revenueCatData.isPremium,
72
+ periodType: revenueCatData.periodType,
73
+ }
74
+ );
75
+
76
+ return {
77
+ success: true,
78
+ data: result.finalData ? CreditsMapper.toEntity(result.finalData) : null,
79
+ error: null,
80
+ };
73
81
  }
74
82
 
75
83
  /**
76
84
  * Delegates to DeductCreditsCommand (Command Pattern)
77
85
  */
78
- async deductCredit(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
86
+ async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
79
87
  return this.deductCommand.execute(userId, cost);
80
88
  }
81
-
82
- async hasCredits(userId: string, cost: number = 1): Promise<boolean> {
83
- const res = await this.getCredits(userId);
84
- return !!(res.success && res.data && res.data.credits >= cost);
85
- }
86
-
87
- async syncExpiredStatus(userId: string): Promise<void> {
88
- const db = getFirestore();
89
- if (!db) return;
90
- try {
91
- await updateDoc(this.getRef(db, userId), {
92
- isPremium: false,
93
- status: "expired",
94
- lastUpdatedAt: serverTimestamp()
95
- });
96
- } catch (e) {
97
- if (__DEV__) console.error("[CreditsRepository] Sync expired failed:", e);
98
- }
99
- }
100
89
  }
101
-
102
- export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
@@ -12,7 +12,7 @@ import type { SubscriptionFeature, PaywallTranslations, PaywallLegalUrls } from
12
12
  import { paywallModalStyles as styles } from "./PaywallModal.styles";
13
13
  import { PaywallFeatures } from "./PaywallFeatures";
14
14
  import { PaywallFooter } from "./PaywallFooter";
15
- import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
15
+ import { usePurchaseLoadingStore, selectIsPurchasing } from "../../subscription/presentation/stores";
16
16
 
17
17
  /** Trial eligibility info per product */
18
18
  export interface TrialEligibilityInfo {
@@ -18,16 +18,47 @@ export type { FirebaseAuthLike, CreditPackageConfig, SubscriptionInitConfig } fr
18
18
 
19
19
  export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
20
20
  const {
21
- apiKey, apiKeyIos, apiKeyAndroid, entitlementId, credits,
22
- getAnonymousUserId, getFirebaseAuth, showAuthModal,
23
- onCreditsUpdated, creditPackages,
21
+ apiKey,
22
+ apiKeyIos,
23
+ apiKeyAndroid,
24
+ entitlementId,
25
+ credits,
26
+ getAnonymousUserId,
27
+ getFirebaseAuth,
28
+ showAuthModal,
29
+ onCreditsUpdated,
30
+ creditPackages,
24
31
  } = config;
25
32
 
26
- const key = Platform.OS === 'ios' ? (apiKeyIos || apiKey || '') : (apiKeyAndroid || apiKey || '');
27
- if (!key) throw new Error('API key required');
33
+ const key = Platform.OS === 'ios'
34
+ ? (apiKeyIos || apiKey)
35
+ : (apiKeyAndroid || apiKey);
36
+
37
+ if (!key) {
38
+ throw new Error('API key required');
39
+ }
40
+
41
+ if (!creditPackages) {
42
+ throw new Error('creditPackages is required');
43
+ }
44
+
45
+ if (!creditPackages.identifierPattern) {
46
+ throw new Error('creditPackages.identifierPattern is required');
47
+ }
48
+
49
+ if (!creditPackages.amounts) {
50
+ throw new Error('creditPackages.amounts is required');
51
+ }
52
+
53
+ if (!getAnonymousUserId) {
54
+ throw new Error('getAnonymousUserId is required');
55
+ }
28
56
 
29
57
  // 1. Configure Repository
30
- configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
58
+ configureCreditsRepository({
59
+ ...credits,
60
+ creditPackageAmounts: creditPackages.amounts
61
+ });
31
62
 
32
63
  // 2. Setup Sync Service
33
64
  const syncService = new SubscriptionSyncService(entitlementId);
@@ -37,7 +68,7 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
37
68
  config: {
38
69
  apiKey: key,
39
70
  entitlementIdentifier: entitlementId,
40
- consumableProductIdentifiers: [creditPackages?.identifierPattern || 'credit'],
71
+ consumableProductIdentifiers: [creditPackages.identifierPattern],
41
72
  onPurchaseCompleted: (u: string, p: string, c: any, s: any) => syncService.handlePurchase(u, p, c, s),
42
73
  onRenewalDetected: (u: string, p: string, expires: string, c: any) => syncService.handleRenewal(u, p, expires, c),
43
74
  onPremiumStatusChanged: (u: string, isP: boolean, pId: any, exp: any, willR: any, pt: any) => syncService.handlePremiumStatusChanged(u, isP, pId, exp, willR, pt),
@@ -50,28 +81,38 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
50
81
  // 4. Configure Auth aware actions
51
82
  configureAuthProvider({
52
83
  isAuthenticated: () => {
53
- const u = getFirebaseAuth()?.currentUser;
84
+ const auth = getFirebaseAuth();
85
+ if (!auth) {
86
+ throw new Error("Firebase auth is not available");
87
+ }
88
+
89
+ const u = auth.currentUser;
54
90
  return !!(u && !u.isAnonymous);
55
91
  },
56
92
  showAuthModal,
57
93
  });
58
94
 
59
- const initializeInBackground = async (userId?: string) => {
60
- try {
61
- await SubscriptionManager.initialize(userId);
62
- if (__DEV__) console.log('[SubscriptionInitializer] Background init complete');
63
- } catch (error) {
64
- if (__DEV__) console.log('[SubscriptionInitializer] Background init failed (non-critical):', error);
95
+ const initializeInBackground = async (userId: string): Promise<void> => {
96
+ await SubscriptionManager.initialize(userId);
97
+ if (__DEV__) {
98
+ console.log('[SubscriptionInitializer] Background init complete');
65
99
  }
66
100
  };
67
101
 
68
102
  // 5. Start Background Init
69
- const initialUserId = getCurrentUserId(getFirebaseAuth);
70
- initializeInBackground(initialUserId);
103
+ const auth = getFirebaseAuth();
104
+ if (!auth) {
105
+ throw new Error("Firebase auth is not available");
106
+ }
107
+
108
+ const initialUserId = getCurrentUserId(() => auth);
109
+ await initializeInBackground(initialUserId);
71
110
 
72
111
  // 6. Listen for Auth Changes
73
- setupAuthStateListener(getFirebaseAuth, (newUserId) => {
74
- if (__DEV__) console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
112
+ setupAuthStateListener(() => auth, (newUserId) => {
113
+ if (__DEV__) {
114
+ console.log('[SubscriptionInitializer] Auth changed, re-init:', newUserId);
115
+ }
75
116
  initializeInBackground(newUserId);
76
117
  });
77
118
  };
@@ -13,38 +13,38 @@ export interface FirebaseAuthLike {
13
13
  }
14
14
 
15
15
  export interface CreditPackageConfig {
16
- identifierPattern?: string;
17
- amounts?: Record<string, number>;
16
+ identifierPattern: string;
17
+ amounts: Record<string, number>;
18
18
  }
19
19
 
20
20
  export interface SubscriptionInitConfig {
21
- apiKey?: string;
22
- apiKeyIos?: string;
23
- apiKeyAndroid?: string;
21
+ apiKey: string;
22
+ apiKeyIos: string;
23
+ apiKeyAndroid: string;
24
24
  entitlementId: string;
25
25
  credits: CreditsConfig;
26
26
  getAnonymousUserId: () => Promise<string>;
27
27
  getFirebaseAuth: () => FirebaseAuthLike | null;
28
28
  showAuthModal: () => void;
29
- onCreditsUpdated?: (userId: string) => void;
30
- creditPackages?: CreditPackageConfig;
31
- timeoutMs?: number;
32
- authStateTimeoutMs?: number;
29
+ onCreditsUpdated: (userId: string) => void;
30
+ creditPackages: CreditPackageConfig;
31
+ timeoutMs: number;
32
+ authStateTimeoutMs: number;
33
33
  }
34
34
 
35
35
  export interface InitializeCreditsMetadata {
36
- productId?: string;
37
- source?: PurchaseSource;
38
- type?: PurchaseType;
39
- expirationDate?: string | null;
40
- willRenew?: boolean;
41
- originalTransactionId?: string;
42
- isPremium?: boolean;
43
- periodType?: PeriodType;
36
+ productId: string;
37
+ source: PurchaseSource;
38
+ type: PurchaseType;
39
+ expirationDate: string | null;
40
+ willRenew: boolean | null;
41
+ originalTransactionId: string | null;
42
+ isPremium: boolean;
43
+ periodType: PeriodType | null;
44
44
  }
45
45
 
46
46
  export interface InitializationResult {
47
- credits: number;
48
- alreadyProcessed?: boolean;
49
- finalData?: UserCreditsDocumentRead;
47
+ credits: number;
48
+ alreadyProcessed: boolean;
49
+ finalData: UserCreditsDocumentRead | null;
50
50
  }
@@ -15,48 +15,67 @@ export interface RestoreResultInfo {
15
15
 
16
16
  export class PackageHandler {
17
17
  constructor(
18
- private service: IRevenueCatService | null,
18
+ private service: IRevenueCatService,
19
19
  private entitlementId: string
20
20
  ) { }
21
21
 
22
- setService = (service: IRevenueCatService | null) => { this.service = service; };
22
+ setService(service: IRevenueCatService): void {
23
+ this.service = service;
24
+ }
23
25
 
24
26
  async fetchPackages(): Promise<PurchasesPackage[]> {
25
- if (!this.service?.isInitialized()) return [];
26
- try {
27
- const offering = await this.service.fetchOfferings();
28
- return offering?.availablePackages ?? [];
29
- } catch (error) {
30
- if (__DEV__) console.error('[PackageHandler] fetchOfferings failed:', error);
31
- return [];
27
+ if (!this.service.isInitialized()) {
28
+ throw new Error("Service not initialized");
32
29
  }
30
+
31
+ const offering = await this.service.fetchOfferings();
32
+
33
+ if (!offering) {
34
+ throw new Error("No offerings available");
35
+ }
36
+
37
+ const packages = offering.availablePackages;
38
+ if (!packages) {
39
+ throw new Error("No packages available in offering");
40
+ }
41
+
42
+ return packages;
33
43
  }
34
44
 
35
45
  async purchase(pkg: PurchasesPackage, userId: string): Promise<boolean> {
36
- if (!this.service?.isInitialized()) return false;
37
- try {
38
- const result = await this.service.purchasePackage(pkg, userId);
39
- return result.success;
40
- } catch (error) {
41
- if (__DEV__) console.error('[PackageHandler] Purchase failed:', error);
42
- return false;
46
+ if (!this.service.isInitialized()) {
47
+ throw new Error("Service not initialized");
43
48
  }
49
+
50
+ const result = await this.service.purchasePackage(pkg, userId);
51
+ return result.success;
44
52
  }
45
53
 
46
54
  async restore(userId: string): Promise<RestoreResultInfo> {
47
- if (!this.service?.isInitialized()) return { success: false, productId: null };
48
- try {
49
- const result = await this.service.restorePurchases(userId);
50
- let productId: string | null = null;
51
- if (result.success && result.customerInfo) {
52
- const entitlement = getPremiumEntitlement(result.customerInfo, this.entitlementId);
53
- if (entitlement) productId = entitlement.productIdentifier;
54
- }
55
- return { success: result.success, productId };
56
- } catch (error) {
57
- if (__DEV__) console.error('[PackageHandler] Restore failed:', error);
55
+ if (!this.service.isInitialized()) {
56
+ throw new Error("Service not initialized");
57
+ }
58
+
59
+ const result = await this.service.restorePurchases(userId);
60
+
61
+ if (!result.success) {
58
62
  return { success: false, productId: null };
59
63
  }
64
+
65
+ if (!result.customerInfo) {
66
+ return { success: true, productId: null };
67
+ }
68
+
69
+ const entitlement = getPremiumEntitlement(result.customerInfo, this.entitlementId);
70
+
71
+ if (!entitlement) {
72
+ return { success: true, productId: null };
73
+ }
74
+
75
+ return {
76
+ success: true,
77
+ productId: entitlement.productIdentifier,
78
+ };
60
79
  }
61
80
 
62
81
  checkPremiumStatusFromInfo(customerInfo: CustomerInfo): PremiumStatus {
@@ -14,7 +14,7 @@ import { SubscriptionInternalState } from "./SubscriptionInternalState";
14
14
  export interface SubscriptionManagerConfig {
15
15
  config: RevenueCatConfig;
16
16
  apiKey: string;
17
- getAnonymousUserId?: () => Promise<string>;
17
+ getAnonymousUserId: () => Promise<string>;
18
18
  }
19
19
 
20
20
  class SubscriptionManagerImpl {
@@ -25,97 +25,161 @@ class SubscriptionManagerImpl {
25
25
 
26
26
  configure(config: SubscriptionManagerConfig): void {
27
27
  this.managerConfig = config;
28
- this.packageHandler = new PackageHandler(null, config.config.entitlementIdentifier);
29
- if (config.getAnonymousUserId) this.state.userIdProvider.configure(config.getAnonymousUserId);
28
+ this.state.userIdProvider.configure(config.getAnonymousUserId);
29
+ }
30
+
31
+ private ensurePackageHandlerInitialized(): void {
32
+ if (this.packageHandler) {
33
+ return;
34
+ }
35
+
36
+ if (!this.serviceInstance) {
37
+ throw new Error("Service instance not available");
38
+ }
39
+
40
+ if (!this.managerConfig) {
41
+ throw new Error("Manager not configured");
42
+ }
43
+
44
+ this.packageHandler = new PackageHandler(
45
+ this.serviceInstance,
46
+ this.managerConfig.config.entitlementIdentifier
47
+ );
30
48
  }
31
49
 
32
50
  private ensureConfigured(): void {
33
- if (!this.managerConfig || !this.packageHandler) throw new Error("SubscriptionManager not configured");
51
+ if (!this.managerConfig) {
52
+ throw new Error("SubscriptionManager not configured");
53
+ }
34
54
  }
35
55
 
36
- async initialize(userId?: string): Promise<boolean> {
56
+ async initialize(userId: string): Promise<boolean> {
37
57
  this.ensureConfigured();
38
- const effectiveUserId = userId || (await this.state.userIdProvider.getOrCreateAnonymousUserId());
39
- const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(effectiveUserId);
40
58
 
41
- if (!shouldInit && existingPromise) return existingPromise;
59
+ const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(userId);
60
+
61
+ if (!shouldInit && existingPromise) {
62
+ return existingPromise;
63
+ }
42
64
 
43
65
  const promise = (async () => {
44
- try {
45
- await initializeRevenueCatService(this.managerConfig!.config);
46
- this.serviceInstance = getRevenueCatService();
47
- if (!this.serviceInstance) {
48
- if (__DEV__) {
49
- console.error('[SubscriptionManager] Service instance not available after initialization');
50
- }
51
- return false;
52
- }
53
- this.packageHandler!.setService(this.serviceInstance);
54
- const result = await this.serviceInstance.initialize(effectiveUserId);
55
- return result.success;
56
- } catch (error) {
57
- if (__DEV__) {
58
- console.error('[SubscriptionManager] Initialization failed:', error);
59
- }
60
- return false;
66
+ await initializeRevenueCatService(this.managerConfig!.config);
67
+ this.serviceInstance = getRevenueCatService();
68
+
69
+ if (!this.serviceInstance) {
70
+ throw new Error("Service instance not available after initialization");
61
71
  }
72
+
73
+ this.ensurePackageHandlerInitialized();
74
+ const result = await this.serviceInstance.initialize(userId);
75
+ return result.success;
62
76
  })();
63
77
 
64
- this.state.initCache.setPromise(promise, effectiveUserId);
78
+ this.state.initCache.setPromise(promise, userId);
65
79
  return promise;
66
80
  }
67
81
 
68
82
  isInitializedForUser(userId: string): boolean {
69
- return this.serviceInstance?.isInitialized() === true && this.state.initCache.getCurrentUserId() === userId;
83
+ if (!this.serviceInstance) {
84
+ return false;
85
+ }
86
+
87
+ if (!this.serviceInstance.isInitialized()) {
88
+ return false;
89
+ }
90
+
91
+ return this.state.initCache.getCurrentUserId() === userId;
70
92
  }
71
93
 
72
94
  async getPackages(): Promise<PurchasesPackage[]> {
73
95
  this.ensureConfigured();
96
+
74
97
  if (!this.serviceInstance) {
75
98
  this.serviceInstance = getRevenueCatService();
76
- this.packageHandler!.setService(this.serviceInstance);
77
99
  }
100
+
101
+ if (!this.serviceInstance) {
102
+ throw new Error("Service instance not available");
103
+ }
104
+
105
+ this.ensurePackageHandlerInitialized();
78
106
  return this.packageHandler!.fetchPackages();
79
107
  }
80
108
 
81
109
  async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
82
110
  this.ensureConfigured();
111
+
83
112
  const userId = this.state.initCache.getCurrentUserId();
84
- if (!userId) return false;
113
+ if (!userId) {
114
+ throw new Error("No current user found");
115
+ }
116
+
117
+ this.ensurePackageHandlerInitialized();
85
118
  return this.packageHandler!.purchase(pkg, userId);
86
119
  }
87
120
 
88
121
  async restore(): Promise<RestoreResultInfo> {
89
122
  this.ensureConfigured();
123
+
90
124
  const userId = this.state.initCache.getCurrentUserId();
91
- if (!userId) return { success: false, productId: null };
125
+ if (!userId) {
126
+ throw new Error("No current user found");
127
+ }
128
+
129
+ this.ensurePackageHandlerInitialized();
92
130
  return this.packageHandler!.restore(userId);
93
131
  }
94
132
 
95
133
  async checkPremiumStatus(): Promise<PremiumStatus> {
96
134
  this.ensureConfigured();
135
+
97
136
  const userId = this.state.initCache.getCurrentUserId();
98
- if (!userId) return { isPremium: false, expirationDate: null };
137
+ if (!userId) {
138
+ throw new Error("No current user found");
139
+ }
140
+
141
+ if (!this.serviceInstance) {
142
+ throw new Error("Service instance not available");
143
+ }
144
+
145
+ const customerInfo = await this.serviceInstance.getCustomerInfo();
99
146
 
100
- try {
101
- const customerInfo = await this.serviceInstance?.getCustomerInfo();
102
- if (customerInfo) return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
103
- } catch (error) {
104
- throw error;
147
+ if (!customerInfo) {
148
+ throw new Error("Customer info not available");
105
149
  }
106
- return { isPremium: false, expirationDate: null };
150
+
151
+ this.ensurePackageHandlerInitialized();
152
+ return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
107
153
  }
108
154
 
109
155
  async reset(): Promise<void> {
110
- if (this.serviceInstance) await this.serviceInstance.reset();
156
+ if (this.serviceInstance) {
157
+ await this.serviceInstance.reset();
158
+ }
159
+
111
160
  this.state.reset();
112
161
  this.serviceInstance = null;
113
162
  }
114
163
 
115
- // Helper status checks
116
- isConfigured = () => !!this.managerConfig;
117
- isInitialized = () => this.serviceInstance?.isInitialized() ?? false;
118
- getEntitlementId = () => this.managerConfig?.config.entitlementIdentifier || null;
164
+ isConfigured(): boolean {
165
+ return this.managerConfig !== null;
166
+ }
167
+
168
+ isInitialized(): boolean {
169
+ if (!this.serviceInstance) {
170
+ return false;
171
+ }
172
+
173
+ return this.serviceInstance.isInitialized();
174
+ }
175
+
176
+ getEntitlementId(): string {
177
+ if (!this.managerConfig) {
178
+ throw new Error("SubscriptionManager not configured");
179
+ }
180
+
181
+ return this.managerConfig.config.entitlementIdentifier;
182
+ }
119
183
  }
120
184
 
121
185
  export const SubscriptionManager = new SubscriptionManagerImpl();
@@ -15,14 +15,16 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
15
15
 
16
16
  try {
17
17
  const customerInfo = await Purchases.restorePurchases();
18
- const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
18
+ const entitlement = customerInfo.entitlements.active[deps.config.entitlementIdentifier];
19
+ const isPremium = !!entitlement;
20
+ const productId = entitlement?.productIdentifier ?? null;
19
21
 
20
22
  if (isPremium) {
21
23
  await syncPremiumStatus(deps.config, userId, customerInfo);
22
24
  }
23
25
  await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
24
26
 
25
- return { success: true, isPremium, customerInfo };
27
+ return { success: true, isPremium, productId, customerInfo };
26
28
  } catch (error) {
27
29
  throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
28
30
  }
@@ -19,8 +19,7 @@ const configurationState = {
19
19
  configurationPromise: null as Promise<ReturnType<typeof initializeSDK>> | null,
20
20
  };
21
21
 
22
- // Simple lock mechanism to prevent concurrent configurations
23
- let configurationLocks = new Set<string>();
22
+ // Simple lock mechanism to prevent concurrent configurations (implementation deferred)
24
23
 
25
24
  function configureLogHandler(): void {
26
25
  if (configurationState.isLogHandlerConfigured) return;
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { CustomerInfo } from "react-native-purchases";
8
- import { detectPackageType, type SubscriptionPackageType } from "../../../../utils/packageTypeDetector";
8
+ import { detectPackageType } from "../../../../utils/packageTypeDetector";
9
9
 
10
10
  export interface RenewalState {
11
11
  previousExpirationDate: string | null;