@umituz/react-native-subscription 2.14.82 → 2.14.83

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.
@@ -1,212 +1,78 @@
1
1
  /**
2
2
  * Credits Repository
3
- *
4
- * Firestore operations for user credits management.
5
- * Extends BaseRepository from @umituz/react-native-firebase.
6
3
  */
7
4
 
8
- import {
9
- doc,
10
- getDoc,
11
- runTransaction,
12
- serverTimestamp,
13
- type Firestore,
14
- type Transaction,
15
- } from "firebase/firestore";
5
+ import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
16
6
  import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
17
- import type {
18
- CreditType,
19
- CreditsConfig,
20
- CreditsResult,
21
- DeductCreditsResult,
22
- } from "../../domain/entities/Credits";
7
+ import type { CreditType, CreditsConfig, CreditsResult, DeductCreditsResult } from "../../domain/entities/Credits";
23
8
  import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
24
9
  import { initializeCreditsTransaction } from "../services/CreditsInitializer";
25
10
  import { detectPackageType } from "../../utils/packageTypeDetector";
26
11
  import { getCreditAllocation } from "../../utils/creditMapper";
27
12
 
28
- declare const __DEV__: boolean;
29
-
30
13
  export class CreditsRepository extends BaseRepository {
31
- private config: CreditsConfig;
32
-
33
- constructor(config: CreditsConfig) {
34
- super();
35
- this.config = config;
36
- }
14
+ constructor(private config: CreditsConfig) { super(); }
37
15
 
38
- private getCreditsDocRef(db: Firestore, userId: string) {
39
- if (this.config.useUserSubcollection) {
40
- // Path: users/{userId}/credits/balance - credits stored in user subcollection
41
- return doc(db, "users", userId, "credits", "balance");
42
- }
43
- return doc(db, this.config.collectionName, userId);
16
+ private getRef(db: Firestore, userId: string) {
17
+ return this.config.useUserSubcollection
18
+ ? doc(db, "users", userId, "credits", "balance")
19
+ : doc(db, this.config.collectionName, userId);
44
20
  }
45
21
 
46
22
  async getCredits(userId: string): Promise<CreditsResult> {
47
23
  const db = getFirestore();
48
- if (!db) {
49
- return {
50
- success: false,
51
- error: { message: "Database not available", code: "DB_NOT_AVAILABLE" },
52
- };
53
- }
54
-
24
+ if (!db) return { success: false, error: { message: "No DB", code: "DB_ERR" } };
55
25
  try {
56
- const creditsRef = this.getCreditsDocRef(db, userId);
57
- const snapshot = await getDoc(creditsRef);
58
-
59
- if (!snapshot.exists()) {
60
- return { success: true, data: undefined };
61
- }
62
-
63
- const data = snapshot.data() as UserCreditsDocumentRead;
64
- return {
65
- success: true,
66
- data: {
67
- textCredits: data.textCredits,
68
- imageCredits: data.imageCredits,
69
- purchasedAt: data.purchasedAt?.toDate?.() || new Date(),
70
- lastUpdatedAt: data.lastUpdatedAt?.toDate?.() || new Date(),
71
- },
72
- };
73
- } catch (error) {
74
- return {
75
- success: false,
76
- error: {
77
- message: error instanceof Error ? error.message : "Failed to get credits",
78
- code: "FETCH_FAILED",
79
- },
80
- };
81
- }
26
+ const snap = await getDoc(this.getRef(db, userId));
27
+ if (!snap.exists()) return { success: true, data: undefined };
28
+ const d = snap.data() as UserCreditsDocumentRead;
29
+ return { success: true, data: { textCredits: d.textCredits, imageCredits: d.imageCredits, purchasedAt: d.purchasedAt?.toDate?.() || new Date(), lastUpdatedAt: d.lastUpdatedAt?.toDate?.() || new Date() } };
30
+ } catch (e: any) { return { success: false, error: { message: e.message, code: "FETCH_ERR" } }; }
82
31
  }
83
32
 
84
- async initializeCredits(
85
- userId: string,
86
- purchaseId?: string,
87
- productId?: string
88
- ): Promise<CreditsResult> {
33
+ async initializeCredits(userId: string, purchaseId?: string, productId?: string): Promise<CreditsResult> {
89
34
  const db = getFirestore();
90
- if (!db) {
91
- return {
92
- success: false,
93
- error: { message: "Database not available", code: "INIT_FAILED" },
94
- };
95
- }
96
-
35
+ if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
97
36
  try {
98
- const creditsRef = this.getCreditsDocRef(db, userId);
99
-
100
- // Determine credit allocation based on product ID
101
- let configToUse = this.config;
102
-
37
+ let cfg = { ...this.config };
103
38
  if (productId) {
104
- // First check credit package amounts (for consumable credit packages)
105
- const creditPackageAmount = this.config.creditPackageAmounts?.[productId];
106
-
107
- if (creditPackageAmount) {
108
- // Credit package: use the configured amount
109
- if (__DEV__) {
110
- console.log("[CreditsRepository] Credit package detected:", { productId, amount: creditPackageAmount });
111
- }
112
- configToUse = {
113
- ...this.config,
114
- imageCreditLimit: creditPackageAmount,
115
- textCreditLimit: creditPackageAmount,
116
- };
117
- } else {
118
- // Subscription package: use package type detection
119
- const packageType = detectPackageType(productId);
120
- const allocation = getCreditAllocation(packageType);
121
-
122
- if (allocation) {
123
- configToUse = {
124
- ...this.config,
125
- imageCreditLimit: allocation.imageCredits,
126
- textCreditLimit: allocation.textCredits,
127
- };
128
- }
39
+ const amt = this.config.creditPackageAmounts?.[productId];
40
+ if (amt) cfg = { ...cfg, imageCreditLimit: amt, textCreditLimit: amt };
41
+ else {
42
+ const alloc = getCreditAllocation(detectPackageType(productId));
43
+ if (alloc) cfg = { ...cfg, imageCreditLimit: alloc.imageCredits, textCreditLimit: alloc.textCredits };
129
44
  }
130
45
  }
131
-
132
- const result = await initializeCreditsTransaction(
133
- db,
134
- creditsRef,
135
- configToUse,
136
- purchaseId
137
- );
138
-
139
- return {
140
- success: true,
141
- data: {
142
- textCredits: result.textCredits,
143
- imageCredits: result.imageCredits,
144
- purchasedAt: new Date(),
145
- lastUpdatedAt: new Date(),
146
- },
147
- };
148
- } catch (error) {
149
- return {
150
- success: false,
151
- error: {
152
- message: error instanceof Error ? error.message : "Failed to initialize credits",
153
- code: "INIT_FAILED",
154
- },
155
- };
156
- }
46
+ const res = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId);
47
+ return { success: true, data: { textCredits: res.textCredits, imageCredits: res.imageCredits, purchasedAt: new Date(), lastUpdatedAt: new Date() } };
48
+ } catch (e: any) { return { success: false, error: { message: e.message, code: "INIT_ERR" } }; }
157
49
  }
158
50
 
159
- async deductCredit(
160
- userId: string,
161
- creditType: CreditType
162
- ): Promise<DeductCreditsResult> {
51
+ async deductCredit(userId: string, type: CreditType): Promise<DeductCreditsResult> {
163
52
  const db = getFirestore();
164
- if (!db) {
165
- return {
166
- success: false,
167
- error: { message: "Database not available", code: "DEDUCT_FAILED" },
168
- };
169
- }
170
-
53
+ if (!db) return { success: false, error: { message: "No DB", code: "ERR" } };
54
+ const field = type === "text" ? "textCredits" : "imageCredits";
171
55
  try {
172
- const creditsRef = this.getCreditsDocRef(db, userId);
173
- const fieldName = creditType === "text" ? "textCredits" : "imageCredits";
174
-
175
- const newCredits = await runTransaction(db, async (transaction: Transaction) => {
176
- const creditsDoc = await transaction.get(creditsRef);
177
- if (!creditsDoc.exists()) throw new Error("NO_CREDITS");
178
-
179
- const currentCredits = creditsDoc.data()[fieldName] as number;
180
- if (currentCredits <= 0) throw new Error("CREDITS_EXHAUSTED");
181
-
182
- const updatedCredits = currentCredits - 1;
183
- transaction.update(creditsRef, {
184
- [fieldName]: updatedCredits,
185
- lastUpdatedAt: serverTimestamp(),
186
- });
187
- return updatedCredits;
56
+ const remaining = await runTransaction(db, async (tx: Transaction) => {
57
+ const docSnap = await tx.get(this.getRef(db, userId));
58
+ if (!docSnap.exists()) throw new Error("NO_CREDITS");
59
+ const current = docSnap.data()[field] as number;
60
+ if (current <= 0) throw new Error("CREDITS_EXHAUSTED");
61
+ const updated = current - 1;
62
+ tx.update(this.getRef(db, userId), { [field]: updated, lastUpdatedAt: serverTimestamp() });
63
+ return updated;
188
64
  });
189
-
190
- return { success: true, remainingCredits: newCredits };
191
- } catch (error) {
192
- const msg = error instanceof Error ? error.message : "Unknown error";
193
- const code = msg === "NO_CREDITS" || msg === "CREDITS_EXHAUSTED" ? msg : "DEDUCT_FAILED";
194
- const message = msg === "NO_CREDITS" ? "No credits found" : msg === "CREDITS_EXHAUSTED" ? "Credits exhausted" : msg;
195
-
196
- return { success: false, error: { message, code } };
65
+ return { success: true, remainingCredits: remaining };
66
+ } catch (e: any) {
67
+ const code = e.message === "NO_CREDITS" || e.message === "CREDITS_EXHAUSTED" ? e.message : "DEDUCT_ERR";
68
+ return { success: false, error: { message: e.message, code } };
197
69
  }
198
70
  }
199
71
 
200
- async hasCredits(userId: string, creditType: CreditType): Promise<boolean> {
201
- const result = await this.getCredits(userId);
202
- if (!result.success || !result.data) return false;
203
- const credits = creditType === "text" ? result.data.textCredits : result.data.imageCredits;
204
- return credits > 0;
72
+ async hasCredits(userId: string, type: CreditType): Promise<boolean> {
73
+ const res = await this.getCredits(userId);
74
+ return !!(res.success && res.data && (type === "text" ? res.data.textCredits : res.data.imageCredits) > 0);
205
75
  }
206
76
  }
207
77
 
208
- export const createCreditsRepository = (
209
- config: CreditsConfig
210
- ): CreditsRepository => {
211
- return new CreditsRepository(config);
212
- };
78
+ export const createCreditsRepository = (c: CreditsConfig) => new CreditsRepository(c);
@@ -1,11 +1,8 @@
1
1
  /**
2
2
  * Subscription Initializer
3
- * Single entry point for subscription system initialization
4
- * Apps just call initializeSubscription with config
5
3
  */
6
4
 
7
5
  import { Platform } from "react-native";
8
- import type { CustomerInfo } from "react-native-purchases";
9
6
  import type { CreditsConfig } from "../../domain/entities/Credits";
10
7
  import { configureCreditsRepository, getCreditsRepository } from "../repositories/CreditsRepositoryProvider";
11
8
  import { SubscriptionManager } from "../../revenuecat/infrastructure/managers/SubscriptionManager";
@@ -16,213 +13,62 @@ export interface FirebaseAuthLike {
16
13
  onAuthStateChanged: (callback: (user: { uid: string; isAnonymous: boolean } | null) => void) => () => void;
17
14
  }
18
15
 
19
- export interface CreditPackageConfig {
20
- /** Identifier pattern to match credit packages (e.g., "credit") */
21
- identifierPattern?: string;
22
- /** Map of productId to credit amounts */
23
- amounts?: Record<string, number>;
24
- }
16
+ export interface CreditPackageConfig { identifierPattern?: string; amounts?: Record<string, number>; }
25
17
 
26
18
  export interface SubscriptionInitConfig {
27
- /** API key for RevenueCat (can provide single key or platform-specific keys) */
28
- apiKey?: string;
29
- /** iOS-specific API key (overrides apiKey if provided on iOS) */
30
- apiKeyIos?: string;
31
- /** Android-specific API key (overrides apiKey if provided on Android) */
32
- apiKeyAndroid?: string;
33
- testStoreKey?: string;
34
- entitlementId: string;
35
- credits: CreditsConfig;
36
- getAnonymousUserId: () => Promise<string>;
37
- getFirebaseAuth: () => FirebaseAuthLike | null;
38
- showAuthModal: () => void;
39
- /** Callback after credits are updated (for cache invalidation) */
40
- onCreditsUpdated?: (userId: string) => void;
41
- /** Credit package configuration for consumable purchases */
42
- creditPackages?: CreditPackageConfig;
43
- timeoutMs?: number;
44
- authStateTimeoutMs?: number;
19
+ apiKey?: string; apiKeyIos?: string; apiKeyAndroid?: string; testStoreKey?: string; entitlementId: string; credits: CreditsConfig;
20
+ getAnonymousUserId: () => Promise<string>; getFirebaseAuth: () => FirebaseAuthLike | null; showAuthModal: () => void;
21
+ onCreditsUpdated?: (userId: string) => void; creditPackages?: CreditPackageConfig; timeoutMs?: number; authStateTimeoutMs?: number;
45
22
  }
46
23
 
47
- /**
48
- * Wait for Firebase Auth state to be ready
49
- * This prevents unnecessary logIn calls that trigger Apple Sign In dialog
50
- */
51
- const waitForAuthState = async (
52
- getFirebaseAuth: () => FirebaseAuthLike | null,
53
- timeoutMs: number
54
- ): Promise<string | undefined> => {
55
- const auth = getFirebaseAuth();
24
+ const waitForAuthState = async (getAuth: () => FirebaseAuthLike | null, timeoutMs: number): Promise<string | undefined> => {
25
+ const auth = getAuth();
56
26
  if (!auth) return undefined;
57
-
58
- // If user already available, return immediately
59
- if (auth.currentUser) {
60
- return auth.currentUser.uid;
61
- }
62
-
63
- // Wait for auth state to settle
64
- return new Promise<string | undefined>((resolve) => {
65
- const unsubscribe = auth.onAuthStateChanged((user) => {
66
- unsubscribe();
67
- resolve(user?.uid || undefined);
68
- });
69
-
70
- // Timeout fallback - don't wait forever
71
- setTimeout(() => {
72
- unsubscribe();
73
- resolve(undefined);
74
- }, timeoutMs);
27
+ if (auth.currentUser) return auth.currentUser.uid;
28
+ return new Promise((resolve) => {
29
+ const unsub = auth.onAuthStateChanged((u) => { unsub(); resolve(u?.uid); });
30
+ setTimeout(() => { unsub(); resolve(undefined); }, timeoutMs);
75
31
  });
76
32
  };
77
33
 
78
- /**
79
- * Check if a product is a credit package
80
- */
81
- const isCreditPackage = (productId: string, pattern?: string): boolean => {
82
- const patternToUse = pattern || "credit";
83
- return productId.toLowerCase().includes(patternToUse.toLowerCase());
84
- };
85
-
86
-
87
- export const initializeSubscription = async (
88
- config: SubscriptionInitConfig,
89
- ): Promise<void> => {
90
- const {
91
- apiKey,
92
- apiKeyIos,
93
- apiKeyAndroid,
94
- testStoreKey,
95
- entitlementId,
96
- credits,
97
- getAnonymousUserId,
98
- getFirebaseAuth,
99
- showAuthModal,
100
- onCreditsUpdated,
101
- creditPackages,
102
- timeoutMs = 10000,
103
- authStateTimeoutMs = 2000,
104
- } = config;
105
-
106
- // Resolve API key based on platform
107
- const resolvedApiKey = Platform.OS === "ios"
108
- ? (apiKeyIos || apiKey || "")
109
- : (apiKeyAndroid || apiKey || "");
110
-
111
- if (!resolvedApiKey) {
112
- throw new Error("RevenueCat API key is required");
113
- }
114
-
115
- // Merge credit package amounts into credits config
116
- const creditsConfigWithPackages = {
117
- ...credits,
118
- creditPackageAmounts: creditPackages?.amounts,
119
- };
120
- configureCreditsRepository(creditsConfigWithPackages);
34
+ const isCreditPkg = (id: string, pat?: string) => id.toLowerCase().includes((pat || "credit").toLowerCase());
121
35
 
122
- // Build consumable product identifiers from credit package pattern
123
- const consumableIdentifiers: string[] = [];
124
- if (creditPackages?.identifierPattern) {
125
- consumableIdentifiers.push(creditPackages.identifierPattern);
126
- } else {
127
- consumableIdentifiers.push("credit");
128
- }
36
+ export const initializeSubscription = async (config: SubscriptionInitConfig): Promise<void> => {
37
+ const { apiKey, apiKeyIos, apiKeyAndroid, testStoreKey, entitlementId, credits, getAnonymousUserId, getFirebaseAuth, showAuthModal, onCreditsUpdated, creditPackages, timeoutMs = 10000, authStateTimeoutMs = 2000 } = config;
129
38
 
130
- // Create onPurchaseCompleted handler for credit packages
131
- const handlePurchaseCompleted = async (
132
- userId: string,
133
- productId: string,
134
- _customerInfo: CustomerInfo
135
- ): Promise<void> => {
136
- const isCredit = isCreditPackage(productId, creditPackages?.identifierPattern);
39
+ const key = Platform.OS === "ios" ? (apiKeyIos || apiKey || "") : (apiKeyAndroid || apiKey || "");
40
+ if (!key) throw new Error("API key required");
137
41
 
138
- if (!isCredit) {
139
- return;
140
- }
42
+ configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
141
43
 
44
+ const onPurchase = async (userId: string, productId: string) => {
45
+ if (!isCreditPkg(productId, creditPackages?.identifierPattern)) return;
142
46
  try {
143
- const repository = getCreditsRepository();
144
- const purchaseId = `purchase_${productId}_${Date.now()}`;
145
-
146
- await repository.initializeCredits(userId, purchaseId, productId);
147
-
148
- if (__DEV__) {
149
- console.log("[SubscriptionInitializer] Credits added for purchase:", {
150
- userId,
151
- productId,
152
- purchaseId,
153
- });
154
- }
155
-
156
- if (onCreditsUpdated) {
157
- onCreditsUpdated(userId);
158
- }
159
- } catch (error) {
160
- if (__DEV__) {
161
- console.error("[SubscriptionInitializer] Failed to add credits:", error);
162
- }
163
- }
47
+ await getCreditsRepository().initializeCredits(userId, `purchase_${productId}_${Date.now()}`, productId);
48
+ onCreditsUpdated?.(userId);
49
+ } catch { /* Silent */ }
164
50
  };
165
51
 
166
- // Create onCreditRenewal handler for subscription renewals
167
- const handleCreditRenewal = async (
168
- userId: string,
169
- productId: string,
170
- renewalId: string
171
- ): Promise<void> => {
52
+ const onRenewal = async (userId: string, productId: string, renewalId: string) => {
172
53
  try {
173
- const repository = getCreditsRepository();
174
- await repository.initializeCredits(userId, renewalId, productId);
175
-
176
- if (__DEV__) {
177
- console.log("[SubscriptionInitializer] Credits renewed:", {
178
- userId,
179
- productId,
180
- renewalId,
181
- });
182
- }
183
-
184
- if (onCreditsUpdated) {
185
- onCreditsUpdated(userId);
186
- }
187
- } catch (error) {
188
- if (__DEV__) {
189
- console.error("[SubscriptionInitializer] Failed to renew credits:", error);
190
- }
191
- }
54
+ await getCreditsRepository().initializeCredits(userId, renewalId, productId);
55
+ onCreditsUpdated?.(userId);
56
+ } catch { /* Silent */ }
192
57
  };
193
58
 
194
59
  SubscriptionManager.configure({
195
- config: {
196
- apiKey: resolvedApiKey,
197
- testStoreKey,
198
- entitlementIdentifier: entitlementId,
199
- consumableProductIdentifiers: consumableIdentifiers,
200
- onCreditRenewal: handleCreditRenewal,
201
- onCreditsUpdated,
202
- onPurchaseCompleted: handlePurchaseCompleted,
203
- },
204
- apiKey: resolvedApiKey,
205
- getAnonymousUserId,
60
+ config: { apiKey: key, testStoreKey, entitlementIdentifier: entitlementId, consumableProductIdentifiers: [creditPackages?.identifierPattern || "credit"], onCreditRenewal: onRenewal, onPurchaseCompleted: onPurchase, onCreditsUpdated },
61
+ apiKey: key, getAnonymousUserId
206
62
  });
207
63
 
208
- // Wait for auth state to get correct user ID
209
- const initialUserId = await waitForAuthState(getFirebaseAuth, authStateTimeoutMs);
210
-
211
- const initPromise = SubscriptionManager.initialize(initialUserId);
212
- const timeoutPromise = new Promise<boolean>((_, reject) =>
213
- setTimeout(
214
- () => reject(new Error("Subscription initialization timeout")),
215
- timeoutMs,
216
- ),
217
- );
218
-
219
- await Promise.race([initPromise, timeoutPromise]);
64
+ const userId = await waitForAuthState(getFirebaseAuth, authStateTimeoutMs);
65
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs));
66
+ await Promise.race([SubscriptionManager.initialize(userId), timeout]);
220
67
 
221
68
  configureAuthProvider({
222
69
  isAuthenticated: () => {
223
- const auth = getFirebaseAuth();
224
- const user = auth?.currentUser;
225
- return !!(user && !user.isAnonymous);
70
+ const u = getFirebaseAuth()?.currentUser;
71
+ return !!(u && !u.isAnonymous);
226
72
  },
227
73
  showAuthModal,
228
74
  });
@@ -0,0 +1,23 @@
1
+ export * from "./useAuthAwarePurchase";
2
+ export * from "./useAuthGate";
3
+ export * from "./useAuthSubscriptionSync";
4
+ export * from "./useCreditChecker";
5
+ export * from "./useCredits";
6
+ export * from "./useCreditsGate";
7
+ export * from "./useDeductCredit";
8
+ export * from "./useInitializeCredits";
9
+ export * from "./useDevTestCallbacks";
10
+ export * from "./useFeatureGate";
11
+ export * from "./usePaywallOperations";
12
+ export * from "./usePaywallVisibility";
13
+ export * from "./usePremium";
14
+ export * from "./usePremiumGate";
15
+ export * from "./usePremiumWithCredits";
16
+ export * from "./useSubscription";
17
+ export * from "./useSubscriptionDetails";
18
+ export * from "./useSubscriptionGate";
19
+ export * from "./useSubscriptionSettingsConfig";
20
+ export * from "./useSubscriptionStatus";
21
+ export * from "./useUserTier";
22
+ export * from "./useUserTierWithRepository";
23
+ export * from "./feedback/usePaywallFeedback";