@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.
|
|
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",
|
|
@@ -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
|
|
54
|
-
let
|
|
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
|
-
|
|
60
|
+
if (existingData && purchaseId && processedPurchases.includes(purchaseId)) {
|
|
61
|
+
return { credits: existingData.credits, alreadyProcessed: true } as InitializationResult & { alreadyProcessed: boolean };
|
|
62
|
+
}
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
-
? [...(
|
|
126
|
-
:
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
168
|
-
if (metadata?.
|
|
169
|
-
|
|
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
|
-
|
|
198
|
-
if (
|
|
199
|
-
|
|
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
|
-
|
|
211
|
-
if (metadata?.
|
|
212
|
-
|
|
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,
|