@umituz/react-native-subscription 2.27.92 → 2.27.93

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditsInitializer.ts +91 -38
  3. package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
  4. package/src/domains/credits/infrastructure/CreditsRepository.ts +44 -58
  5. package/src/domains/paywall/components/PaywallModal.tsx +1 -1
  6. package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
  7. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
  8. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
  9. package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
  10. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
  11. package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
  12. package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
  13. package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
  14. package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
  15. package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
  16. package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
  17. package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
  18. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
  19. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
  20. package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
  21. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
  22. package/src/shared/application/ports/IRevenueCatService.ts +2 -0
  23. package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
  24. package/src/presentation/README.md +0 -125
  25. package/src/presentation/hooks/README.md +0 -156
  26. package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
  27. package/src/presentation/hooks/useCredits.md +0 -103
  28. package/src/presentation/hooks/useDeductCredit.md +0 -100
  29. package/src/presentation/hooks/useFeatureGate.md +0 -112
  30. package/src/presentation/hooks/usePaywall.md +0 -89
  31. package/src/presentation/hooks/usePaywallOperations.md +0 -92
  32. package/src/presentation/hooks/usePaywallVisibility.md +0 -95
  33. package/src/presentation/hooks/usePremium.md +0 -88
  34. package/src/presentation/hooks/useSubscriptionSettingsConfig.md +0 -94
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.92",
3
+ "version": "2.27.93",
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",
@@ -1,13 +1,14 @@
1
1
  import { Platform } from "react-native";
2
2
  import Constants from "expo-constants";
3
+ import {
4
+ getFirestore,
5
+ } from "@umituz/react-native-firebase";
3
6
  import {
4
7
  runTransaction,
5
8
  serverTimestamp,
6
- Timestamp,
7
9
  type Transaction,
8
10
  type DocumentReference,
9
- type Firestore,
10
- } from "@umituz/react-native-firebase";
11
+ } from "firebase/firestore";
11
12
  import type { CreditsConfig } from "../core/Credits";
12
13
  import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
13
14
  import { resolveSubscriptionStatus } from "../../subscription/core/SubscriptionStatus";
@@ -17,41 +18,70 @@ import { creditAllocationContext } from "./credit-strategies/CreditAllocationCon
17
18
  import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
18
19
 
19
20
  export async function initializeCreditsTransaction(
20
- db: Firestore,
21
+ db: ReturnType<typeof getFirestore>,
21
22
  creditsRef: DocumentReference,
22
23
  config: CreditsConfig,
23
- purchaseId?: string,
24
- metadata?: InitializeCreditsMetadata
24
+ purchaseId: string,
25
+ metadata: InitializeCreditsMetadata
25
26
  ): Promise<InitializationResult> {
27
+ if (!db) {
28
+ throw new Error("Firestore instance is not available");
29
+ }
30
+
31
+ if (!metadata.productId) {
32
+ throw new Error("productId is required in metadata");
33
+ }
34
+
26
35
  return runTransaction(db, async (transaction: Transaction) => {
27
36
  const creditsDoc = await transaction.get(creditsRef);
28
37
  const now = serverTimestamp();
29
- const existingData = creditsDoc.exists() ? creditsDoc.data() as UserCreditsDocumentRead : null;
38
+ const existingData = creditsDoc.exists()
39
+ ? creditsDoc.data() as UserCreditsDocumentRead
40
+ : null;
41
+
42
+ if (!existingData) {
43
+ throw new Error("Credits document does not exist");
44
+ }
30
45
 
31
- if (existingData && purchaseId && existingData.processedPurchases?.includes(purchaseId)) {
46
+ if (existingData.processedPurchases.includes(purchaseId)) {
32
47
  return { credits: existingData.credits, alreadyProcessed: true };
33
48
  }
34
49
 
35
- const creditLimit = CreditLimitCalculator.calculate(metadata?.productId, config);
50
+ const creditLimit = CreditLimitCalculator.calculate(metadata.productId, config);
51
+
52
+ const platform = Platform.OS;
53
+ if (platform !== "ios" && platform !== "android") {
54
+ throw new Error(`Invalid platform: ${platform}`);
55
+ }
56
+
57
+ const appVersion = Constants.expoConfig?.version;
58
+ if (!appVersion) {
59
+ throw new Error("appVersion is required in expoConfig");
60
+ }
61
+
36
62
  const { purchaseHistory } = PurchaseMetadataGenerator.generate({
37
- productId: metadata?.productId,
38
- source: metadata?.source,
39
- type: metadata?.type,
40
- creditLimit,
41
- platform: Platform.OS as "ios" | "android",
42
- appVersion: Constants.expoConfig?.version,
63
+ productId: metadata.productId,
64
+ source: metadata.source,
65
+ type: metadata.type,
66
+ creditLimit,
67
+ platform,
68
+ appVersion,
43
69
  }, existingData);
44
70
 
45
- const isPremium = metadata?.isPremium ?? true;
46
- const isExpired = metadata?.expirationDate ? new Date(metadata.expirationDate).getTime() < Date.now() : false;
47
- const status = resolveSubscriptionStatus({
48
- isPremium, willRenew: metadata?.willRenew, isExpired, periodType: metadata?.periodType
71
+ const isPremium = metadata.isPremium;
72
+ const isExpired = metadata.expirationDate
73
+ ? new Date(metadata.expirationDate).getTime() < Date.now()
74
+ : false;
75
+
76
+ const status = resolveSubscriptionStatus({
77
+ isPremium,
78
+ willRenew: metadata.willRenew,
79
+ isExpired,
80
+ periodType: metadata.periodType,
49
81
  });
50
82
 
51
- // Resolve credits using Strategy Pattern
52
- const isStatusSync = purchaseId?.startsWith("status_sync_") ?? false;
83
+ const isStatusSync = purchaseId.startsWith("status_sync_");
53
84
  const isSubscriptionActive = isPremium && !isExpired;
54
- const productId = metadata?.productId;
55
85
 
56
86
  const newCredits = creditAllocationContext.allocate({
57
87
  status,
@@ -59,30 +89,53 @@ export async function initializeCreditsTransaction(
59
89
  existingData,
60
90
  creditLimit,
61
91
  isSubscriptionActive,
62
- productId,
92
+ productId: metadata.productId,
63
93
  });
64
94
 
95
+ const newProcessedPurchases = [...existingData.processedPurchases, purchaseId].slice(-50);
96
+
65
97
  const creditsData: Record<string, any> = {
66
- isPremium, status, credits: newCredits, creditLimit,
67
- lastUpdatedAt: now,
68
- // Increase history window to 50 for better idempotency
69
- processedPurchases: (purchaseId ? [...(existingData?.processedPurchases || []), purchaseId].slice(-50) : existingData?.processedPurchases) || [],
70
- purchaseHistory: purchaseHistory.length ? purchaseHistory : undefined
98
+ isPremium,
99
+ status,
100
+ credits: newCredits,
101
+ creditLimit,
102
+ lastUpdatedAt: now,
103
+ processedPurchases: newProcessedPurchases,
71
104
  };
72
105
 
73
- const isNewPurchaseOrRenewal = purchaseId?.startsWith("purchase_") || purchaseId?.startsWith("renewal_");
74
- if (isNewPurchaseOrRenewal) creditsData.lastPurchaseAt = now;
75
- if (metadata?.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
76
- if (metadata?.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
77
- if (metadata?.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
78
- if (metadata?.productId) {
79
- creditsData.productId = metadata.productId;
80
- creditsData.platform = Platform.OS;
106
+ if (purchaseHistory.length > 0) {
107
+ creditsData.purchaseHistory = purchaseHistory;
108
+ }
109
+
110
+ const isNewPurchaseOrRenewal = purchaseId.startsWith("purchase_")
111
+ || purchaseId.startsWith("renewal_");
112
+
113
+ if (isNewPurchaseOrRenewal) {
114
+ creditsData.lastPurchaseAt = now;
115
+ }
116
+
117
+ if (metadata.expirationDate) {
118
+ creditsData.expirationDate = serverTimestamp();
81
119
  }
82
120
 
121
+ if (metadata.willRenew !== undefined) {
122
+ creditsData.willRenew = metadata.willRenew;
123
+ }
124
+
125
+ if (metadata.originalTransactionId) {
126
+ creditsData.originalTransactionId = metadata.originalTransactionId;
127
+ }
128
+
129
+ creditsData.productId = metadata.productId;
130
+ creditsData.platform = platform;
131
+
83
132
  transaction.set(creditsRef, creditsData, { merge: true });
84
-
85
- const finalData = { ...(existingData || {}), ...creditsData } as UserCreditsDocumentRead;
133
+
134
+ const finalData: UserCreditsDocumentRead = {
135
+ ...existingData,
136
+ ...creditsData,
137
+ };
138
+
86
139
  return { credits: newCredits, finalData };
87
140
  });
88
141
  }
@@ -1,17 +1,17 @@
1
- import type {
2
- PurchaseSource,
3
- PurchaseType,
4
- SubscriptionStatusType,
1
+ import type {
2
+ PurchaseSource,
3
+ PurchaseType,
4
+ SubscriptionStatusType,
5
5
  PeriodType,
6
6
  PackageType,
7
7
  Platform
8
8
  } from "../../subscription/core/SubscriptionConstants";
9
9
 
10
- export type {
11
- PurchaseSource,
12
- PurchaseType,
13
- SubscriptionStatusType,
14
- PeriodType
10
+ export type {
11
+ PurchaseSource,
12
+ PurchaseType,
13
+ SubscriptionStatusType,
14
+ PeriodType
15
15
  };
16
16
 
17
17
  export interface FirestoreTimestamp {
@@ -25,45 +25,45 @@ export interface PurchaseMetadata {
25
25
  source: PurchaseSource;
26
26
  type: PurchaseType;
27
27
  platform: Platform;
28
- appVersion?: string;
28
+ appVersion: string;
29
29
  timestamp: FirestoreTimestamp;
30
30
  }
31
31
 
32
32
  /** Single Source of Truth for user subscription data */
33
33
  export interface UserCreditsDocumentRead {
34
34
  // Core subscription status
35
- isPremium?: boolean;
36
- status?: SubscriptionStatusType;
35
+ isPremium: boolean;
36
+ status: SubscriptionStatusType;
37
37
 
38
38
  // Dates (all from RevenueCat)
39
- purchasedAt?: FirestoreTimestamp;
40
- expirationDate?: FirestoreTimestamp;
41
- lastUpdatedAt?: FirestoreTimestamp;
42
- lastPurchaseAt?: FirestoreTimestamp;
39
+ purchasedAt: FirestoreTimestamp;
40
+ expirationDate: FirestoreTimestamp | null;
41
+ lastUpdatedAt: FirestoreTimestamp;
42
+ lastPurchaseAt: FirestoreTimestamp | null;
43
43
 
44
44
  // RevenueCat subscription details
45
- willRenew?: boolean;
46
- productId?: string;
47
- packageType?: PackageType;
48
- originalTransactionId?: string;
45
+ willRenew: boolean | null;
46
+ productId: string | null;
47
+ packageType: PackageType | null;
48
+ originalTransactionId: string | null;
49
49
 
50
50
  // Trial fields
51
- periodType?: PeriodType;
52
- isTrialing?: boolean;
53
- trialStartDate?: FirestoreTimestamp;
54
- trialEndDate?: FirestoreTimestamp;
55
- trialCredits?: number;
56
- convertedFromTrial?: boolean;
51
+ periodType: PeriodType | null;
52
+ isTrialing: boolean | null;
53
+ trialStartDate: FirestoreTimestamp | null;
54
+ trialEndDate: FirestoreTimestamp | null;
55
+ trialCredits: number | null;
56
+ convertedFromTrial: boolean | null;
57
57
 
58
58
  // Credits
59
59
  credits: number;
60
- creditLimit?: number;
60
+ creditLimit: number;
61
61
 
62
62
  // Metadata
63
- purchaseSource?: PurchaseSource;
64
- purchaseType?: PurchaseType;
65
- platform?: Platform;
66
- appVersion?: string;
67
- processedPurchases?: string[];
68
- purchaseHistory?: PurchaseMetadata[];
63
+ purchaseSource: PurchaseSource | null;
64
+ purchaseType: PurchaseType | null;
65
+ platform: Platform;
66
+ appVersion: string | null;
67
+ processedPurchases: string[];
68
+ purchaseHistory: PurchaseMetadata[];
69
69
  }
@@ -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,60 @@ 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: undefined };
39
+ }
40
+
41
+ const entity = CreditsMapper.toEntity(snap.data() as UserCreditsDocumentRead);
42
+ return { success: true, data: entity };
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) : undefined,
79
+ };
73
80
  }
74
81
 
75
82
  /**
76
83
  * Delegates to DeductCreditsCommand (Command Pattern)
77
84
  */
78
- async deductCredit(userId: string, cost: number = 1): Promise<DeductCreditsResult> {
85
+ async deductCredit(userId: string, cost: number): Promise<DeductCreditsResult> {
79
86
  return this.deductCommand.execute(userId, cost);
80
87
  }
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
88
  }
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
  };
@@ -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 {