@umituz/react-native-subscription 2.27.8 → 2.27.10

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.8",
3
+ "version": "2.27.10",
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",
@@ -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,