@umituz/react-native-subscription 2.27.9 → 2.27.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.27.9",
3
+ "version": "2.27.11",
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",
@@ -74,10 +74,10 @@ export interface CreditsConfig {
74
74
  creditPackageAmounts?: Record<string, number>;
75
75
  /** Credit allocations for different subscription types (weekly, monthly, yearly) */
76
76
  packageAllocations?: PackageAllocationMap;
77
- /** Free credits given to new users on registration (default: 0) */
77
+ /** Enable free credits for new users (default: false) */
78
+ enableFreeCredits?: boolean;
79
+ /** Free credits given to new users on registration (only used when enableFreeCredits: true) */
78
80
  freeCredits?: number;
79
- /** Whether to auto-initialize free credits when user has no credits document (default: true if freeCredits > 0) */
80
- autoInitializeFreeCredits?: boolean;
81
81
  }
82
82
 
83
83
  export interface CreditsResult<T = UserCredits> {
@@ -57,6 +57,8 @@ export interface UserCreditsDocumentRead {
57
57
  // Credits
58
58
  credits: number;
59
59
  creditLimit?: number;
60
+ initialFreeCredits?: number;
61
+ isFreeCredits?: boolean;
60
62
 
61
63
  // Metadata
62
64
  purchaseSource?: PurchaseSource;
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Credits Initializer
3
+ * Handles subscription credit initialization (NOT free credits)
4
+ * Free credits are handled by CreditsRepository.initializeFreeCredits()
5
+ */
6
+
1
7
  import { Platform } from "react-native";
2
8
  import Constants from "expo-constants";
3
9
  import {
@@ -25,17 +31,14 @@ interface InitializationResult {
25
31
  credits: number;
26
32
  }
27
33
 
28
- /** RevenueCat data to save to Firestore (Single Source of Truth) */
29
34
  export interface InitializeCreditsMetadata {
30
35
  productId?: string;
31
36
  source?: PurchaseSource;
32
37
  type?: PurchaseType;
33
- // RevenueCat subscription data
34
38
  expirationDate?: string | null;
35
39
  willRenew?: boolean;
36
40
  originalTransactionId?: string;
37
41
  isPremium?: boolean;
38
- /** RevenueCat period type: NORMAL, INTRO, or TRIAL */
39
42
  periodType?: PeriodType;
40
43
  }
41
44
 
@@ -49,34 +52,23 @@ export async function initializeCreditsTransaction(
49
52
  return runTransaction(db, async (transaction: Transaction) => {
50
53
  const creditsDoc = await transaction.get(creditsRef);
51
54
  const now = serverTimestamp();
55
+ const existingData = creditsDoc.exists() ? creditsDoc.data() as UserCreditsDocumentRead : null;
52
56
 
53
- let newCredits = config.creditLimit;
54
- let purchasedAt = now;
55
- let processedPurchases: string[] = [];
56
-
57
- if (creditsDoc.exists()) {
58
- const existing = creditsDoc.data() as UserCreditsDocumentRead;
59
- processedPurchases = existing.processedPurchases || [];
60
-
61
- if (purchaseId && processedPurchases.includes(purchaseId)) {
62
- return {
63
- credits: existing.credits,
64
- alreadyProcessed: true,
65
- } as any;
66
- }
57
+ let purchasedAt: FieldValue = now;
58
+ let processedPurchases: string[] = existingData?.processedPurchases || [];
67
59
 
68
- newCredits = config.creditLimit;
60
+ if (existingData && purchaseId && processedPurchases.includes(purchaseId)) {
61
+ return { credits: existingData.credits, alreadyProcessed: true } as InitializationResult & { alreadyProcessed: boolean };
62
+ }
69
63
 
70
- if (existing.purchasedAt) {
71
- purchasedAt = existing.purchasedAt as unknown as FieldValue;
72
- }
64
+ if (existingData?.purchasedAt) {
65
+ purchasedAt = existingData.purchasedAt as unknown as FieldValue;
73
66
  }
74
67
 
75
68
  if (purchaseId) {
76
69
  processedPurchases = [...processedPurchases, purchaseId].slice(-10);
77
70
  }
78
71
 
79
- // Detect package type and credit limit from productId
80
72
  const productId = metadata?.productId;
81
73
  const packageType = productId ? detectPackageType(productId) : undefined;
82
74
  const allocation = packageType && packageType !== "unknown"
@@ -84,29 +76,17 @@ export async function initializeCreditsTransaction(
84
76
  : null;
85
77
  const creditLimit = allocation || config.creditLimit;
86
78
 
87
- // Platform and app version
88
79
  const platform = Platform.OS as "ios" | "android";
89
80
  const appVersion = Constants.expoConfig?.version;
90
81
 
91
- // Determine purchase type
92
82
  let purchaseType: PurchaseType = metadata?.type ?? "initial";
93
- if (creditsDoc.exists()) {
94
- const existing = creditsDoc.data() as UserCreditsDocumentRead;
95
- if (existing.packageType && packageType !== "unknown") {
96
- const oldLimit = existing.creditLimit || 0;
97
- const newLimit = creditLimit;
98
- if (newLimit > oldLimit) {
99
- purchaseType = "upgrade";
100
- } else if (newLimit < oldLimit) {
101
- purchaseType = "downgrade";
102
- } else if (purchaseId?.startsWith("renewal_")) {
103
- purchaseType = "renewal";
104
- }
105
- }
83
+ if (existingData?.packageType && packageType !== "unknown") {
84
+ const oldLimit = existingData.creditLimit || 0;
85
+ if (creditLimit > oldLimit) purchaseType = "upgrade";
86
+ else if (creditLimit < oldLimit) purchaseType = "downgrade";
87
+ else if (purchaseId?.startsWith("renewal_")) purchaseType = "renewal";
106
88
  }
107
89
 
108
- // Create purchase metadata for history (only if productId and source exists and packageType detected)
109
- // NOTE: Cannot use serverTimestamp() in arrays, using Date.now() instead
110
90
  const purchaseMetadata: PurchaseMetadata | undefined =
111
91
  productId && metadata?.source && packageType && packageType !== "unknown" ? {
112
92
  productId,
@@ -116,107 +96,59 @@ export async function initializeCreditsTransaction(
116
96
  type: purchaseType,
117
97
  platform,
118
98
  appVersion,
119
- timestamp: Date.now() as any, // Use Date.now() instead of serverTimestamp() for arrays
99
+ timestamp: Date.now() as unknown as PurchaseMetadata["timestamp"],
120
100
  } : undefined;
121
101
 
122
- // Update purchase history (keep last 10, only if metadata exists)
123
- const existing = creditsDoc.exists() ? creditsDoc.data() as UserCreditsDocumentRead : null;
124
102
  const purchaseHistory = purchaseMetadata
125
- ? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
126
- : existing?.purchaseHistory;
103
+ ? [...(existingData?.purchaseHistory || []), purchaseMetadata].slice(-10)
104
+ : existingData?.purchaseHistory;
127
105
 
128
- // Determine subscription status
129
106
  const isPremium = metadata?.isPremium ?? true;
130
107
  const willRenew = metadata?.willRenew;
131
108
  const periodType = metadata?.periodType;
132
109
 
133
- const status = resolveSubscriptionStatus({
134
- isPremium,
135
- willRenew,
136
- isExpired: !isPremium,
137
- periodType,
138
- });
139
-
140
- // Determine credits based on status
141
- // Trial: 5 credits, Trial canceled: 0 credits, Normal: plan-based credits
142
- if (status === SUBSCRIPTION_STATUS.TRIAL) {
143
- newCredits = TRIAL_CONFIG.CREDITS;
144
- } else if (status === SUBSCRIPTION_STATUS.TRIAL_CANCELED) {
145
- newCredits = 0;
146
- }
110
+ const status = resolveSubscriptionStatus({ isPremium, willRenew, isExpired: !isPremium, periodType });
111
+
112
+ let newCredits = creditLimit;
113
+ if (status === SUBSCRIPTION_STATUS.TRIAL) newCredits = TRIAL_CONFIG.CREDITS;
114
+ else if (status === SUBSCRIPTION_STATUS.TRIAL_CANCELED) newCredits = 0;
147
115
 
148
- // Build credits data (Single Source of Truth)
149
116
  const creditsData: Record<string, unknown> = {
150
- // Core subscription
151
117
  isPremium,
152
118
  status,
153
-
154
- // Credits
155
119
  credits: newCredits,
156
120
  creditLimit,
157
-
158
- // Dates
159
121
  purchasedAt,
160
122
  lastUpdatedAt: now,
161
123
  lastPurchaseAt: now,
162
-
163
- // Tracking
164
124
  processedPurchases,
165
125
  };
166
126
 
167
- // RevenueCat subscription data
168
- if (metadata?.expirationDate) {
169
- creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
170
- }
171
- if (metadata?.willRenew !== undefined) {
172
- creditsData.willRenew = metadata.willRenew;
173
- }
174
- if (metadata?.originalTransactionId) {
175
- creditsData.originalTransactionId = metadata.originalTransactionId;
176
- }
177
-
178
- // Package info
179
- if (packageType && packageType !== "unknown") {
180
- creditsData.packageType = packageType;
181
- }
127
+ if (metadata?.expirationDate) creditsData.expirationDate = Timestamp.fromDate(new Date(metadata.expirationDate));
128
+ if (metadata?.willRenew !== undefined) creditsData.willRenew = metadata.willRenew;
129
+ if (metadata?.originalTransactionId) creditsData.originalTransactionId = metadata.originalTransactionId;
130
+ if (packageType && packageType !== "unknown") creditsData.packageType = packageType;
182
131
  if (productId) {
183
132
  creditsData.productId = productId;
184
133
  creditsData.platform = platform;
185
134
  creditsData.appVersion = appVersion;
186
135
  }
187
136
 
188
- // Trial-specific fields
189
137
  const isTrialing = status === SUBSCRIPTION_STATUS.TRIAL || status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
190
-
191
- if (periodType) {
192
- creditsData.periodType = periodType;
193
- }
138
+ if (periodType) creditsData.periodType = periodType;
194
139
  if (isTrialing) {
195
140
  creditsData.isTrialing = status === SUBSCRIPTION_STATUS.TRIAL;
196
141
  creditsData.trialCredits = TRIAL_CONFIG.CREDITS;
197
- // Set trial dates if this is a new trial
198
- if (!existing?.trialStartDate) {
199
- creditsData.trialStartDate = now;
200
- }
201
- if (metadata?.expirationDate) {
202
- creditsData.trialEndDate = Timestamp.fromDate(new Date(metadata.expirationDate));
203
- }
204
- } else if (existing?.isTrialing && isPremium) {
205
- // User converted from trial to paid
142
+ if (!existingData?.trialStartDate) creditsData.trialStartDate = now;
143
+ if (metadata?.expirationDate) creditsData.trialEndDate = Timestamp.fromDate(new Date(metadata.expirationDate));
144
+ } else if (existingData?.isTrialing && isPremium) {
206
145
  creditsData.isTrialing = false;
207
146
  creditsData.convertedFromTrial = true;
208
147
  }
209
148
 
210
- // Purchase metadata
211
- if (metadata?.source) {
212
- creditsData.purchaseSource = metadata.source;
213
- }
214
- if (metadata?.type) {
215
- creditsData.purchaseType = purchaseType;
216
- }
217
- if (purchaseHistory && purchaseHistory.length > 0) {
218
- creditsData.purchaseHistory = purchaseHistory;
219
- }
149
+ if (metadata?.source) creditsData.purchaseSource = metadata.source;
150
+ if (metadata?.type) creditsData.purchaseType = purchaseType;
151
+ if (purchaseHistory?.length) creditsData.purchaseHistory = purchaseHistory;
220
152
 
221
153
  transaction.set(creditsRef, creditsData, { merge: true });
222
154
 
@@ -118,6 +118,25 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
118
118
  console.log('[SubscriptionInitializer] onPremiumStatusChanged:', { userId, isPremium, productId, willRenew, periodType });
119
119
  }
120
120
  try {
121
+ // If not premium and no productId, this is a free user - don't overwrite free credits
122
+ if (!isPremium && !productId) {
123
+ if (__DEV__) {
124
+ console.log('[SubscriptionInitializer] Free user detected, preserving free credits');
125
+ }
126
+ return;
127
+ }
128
+
129
+ // If premium became false (subscription expired/canceled), sync expired status only
130
+ if (!isPremium && productId) {
131
+ await getCreditsRepository().syncExpiredStatus(userId);
132
+ if (__DEV__) {
133
+ console.log('[SubscriptionInitializer] Subscription expired, synced status');
134
+ }
135
+ onCreditsUpdated?.(userId);
136
+ return;
137
+ }
138
+
139
+ // Premium user - initialize credits with subscription data
121
140
  const revenueCatData: RevenueCatData = {
122
141
  expirationDate: expiresAt ?? null,
123
142
  willRenew: willRenew ?? false,
@@ -115,7 +115,8 @@ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCre
115
115
  const isConfigured = isCreditsRepositoryConfigured();
116
116
  const config = getCreditsConfig();
117
117
  const freeCredits = config.freeCredits ?? 0;
118
- const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
118
+ // Free credits only enabled when explicitly set to true AND freeCredits > 0
119
+ const isFreeCreditsEnabled = config.enableFreeCredits === true && freeCredits > 0;
119
120
 
120
121
  // Check if THIS user's init is in progress (shared across all hook instances)
121
122
  const isInitializing = userId ? inProgressSet.has(userId) : false;
@@ -127,7 +128,7 @@ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCre
127
128
  isRegisteredUser &&
128
129
  isConfigured &&
129
130
  !hasCredits &&
130
- autoInit &&
131
+ isFreeCreditsEnabled &&
131
132
  !freeCreditsInitAttempted.has(userId);
132
133
 
133
134
  // Stable callback reference
@@ -143,12 +144,12 @@ export function useFreeCreditsInit(params: UseFreeCreditsInitParams): UseFreeCre
143
144
  if (!freeCreditsInitAttempted.has(userId)) {
144
145
  initializeFreeCreditsForUser(userId, stableOnComplete);
145
146
  }
146
- } else if (querySuccess && isAnonymous && !hasCredits && autoInit) {
147
+ } else if (querySuccess && isAnonymous && !hasCredits && isFreeCreditsEnabled) {
147
148
  if (typeof __DEV__ !== "undefined" && __DEV__) {
148
149
  console.log("[useFreeCreditsInit] Skipping - anonymous user must register first");
149
150
  }
150
151
  }
151
- }, [needsInit, userId, querySuccess, isAnonymous, hasCredits, autoInit, stableOnComplete]);
152
+ }, [needsInit, userId, querySuccess, isAnonymous, hasCredits, isFreeCreditsEnabled, stableOnComplete]);
152
153
 
153
154
  return {
154
155
  isInitializing: isInitializing || needsInit,