@umituz/react-native-subscription 2.9.4 → 2.9.6
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.9.
|
|
3
|
+
"version": "2.9.6",
|
|
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",
|
|
@@ -67,4 +67,4 @@
|
|
|
67
67
|
"README.md",
|
|
68
68
|
"LICENSE"
|
|
69
69
|
]
|
|
70
|
-
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
export interface FirestoreTimestamp {
|
|
3
|
+
toDate: () => Date;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Document structure when READING from Firestore
|
|
7
|
+
export interface UserCreditsDocumentRead {
|
|
8
|
+
textCredits: number;
|
|
9
|
+
imageCredits: number;
|
|
10
|
+
purchasedAt?: FirestoreTimestamp;
|
|
11
|
+
lastUpdatedAt?: FirestoreTimestamp;
|
|
12
|
+
lastPurchaseAt?: FirestoreTimestamp;
|
|
13
|
+
processedPurchases?: string[];
|
|
14
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
|
|
1
2
|
/**
|
|
2
3
|
* Credits Repository
|
|
3
4
|
*
|
|
4
5
|
* Firestore operations for user credits management.
|
|
5
6
|
* Extends BaseRepository from @umituz/react-native-firebase.
|
|
6
|
-
*
|
|
7
|
-
* Generic and reusable - accepts config from main app.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import {
|
|
@@ -12,8 +11,8 @@ import {
|
|
|
12
11
|
getDoc,
|
|
13
12
|
runTransaction,
|
|
14
13
|
serverTimestamp,
|
|
15
|
-
type FieldValue,
|
|
16
14
|
type Firestore,
|
|
15
|
+
type Transaction,
|
|
17
16
|
} from "firebase/firestore";
|
|
18
17
|
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
19
18
|
import type {
|
|
@@ -22,20 +21,8 @@ import type {
|
|
|
22
21
|
CreditsResult,
|
|
23
22
|
DeductCreditsResult,
|
|
24
23
|
} from "../../domain/entities/Credits";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
toDate: () => Date;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Document structure when READING from Firestore
|
|
31
|
-
interface UserCreditsDocumentRead {
|
|
32
|
-
textCredits: number;
|
|
33
|
-
imageCredits: number;
|
|
34
|
-
purchasedAt?: FirestoreTimestamp;
|
|
35
|
-
lastUpdatedAt?: FirestoreTimestamp;
|
|
36
|
-
lastPurchaseAt?: FirestoreTimestamp;
|
|
37
|
-
processedPurchases?: string[];
|
|
38
|
-
}
|
|
24
|
+
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
25
|
+
import { initializeCreditsTransaction } from "../services/CreditsInitializer";
|
|
39
26
|
|
|
40
27
|
export class CreditsRepository extends BaseRepository {
|
|
41
28
|
private config: CreditsConfig;
|
|
@@ -83,8 +70,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
83
70
|
return {
|
|
84
71
|
success: false,
|
|
85
72
|
error: {
|
|
86
|
-
message:
|
|
87
|
-
error instanceof Error ? error.message : "Failed to get credits",
|
|
73
|
+
message: error instanceof Error ? error.message : "Failed to get credits",
|
|
88
74
|
code: "FETCH_FAILED",
|
|
89
75
|
},
|
|
90
76
|
};
|
|
@@ -105,53 +91,12 @@ export class CreditsRepository extends BaseRepository {
|
|
|
105
91
|
|
|
106
92
|
try {
|
|
107
93
|
const creditsRef = this.getCreditsDocRef(db, userId);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let newImageCredits = this.config.imageCreditLimit;
|
|
115
|
-
let purchasedAt = now;
|
|
116
|
-
let processedPurchases: string[] = [];
|
|
117
|
-
|
|
118
|
-
if (creditsDoc.exists()) {
|
|
119
|
-
const existing = creditsDoc.data() as UserCreditsDocumentRead;
|
|
120
|
-
processedPurchases = existing.processedPurchases || [];
|
|
121
|
-
|
|
122
|
-
if (purchaseId && processedPurchases.includes(purchaseId)) {
|
|
123
|
-
return {
|
|
124
|
-
textCredits: existing.textCredits,
|
|
125
|
-
imageCredits: existing.imageCredits,
|
|
126
|
-
alreadyProcessed: true,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
newTextCredits =
|
|
131
|
-
(existing.textCredits || 0) + this.config.textCreditLimit;
|
|
132
|
-
newImageCredits =
|
|
133
|
-
(existing.imageCredits || 0) + this.config.imageCreditLimit;
|
|
134
|
-
// Keep existing purchasedAt if available, otherwise use server timestamp
|
|
135
|
-
if (existing.purchasedAt) {
|
|
136
|
-
purchasedAt = existing.purchasedAt as unknown as FieldValue;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (purchaseId) {
|
|
141
|
-
processedPurchases = [...processedPurchases, purchaseId].slice(-10);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
transaction.set(creditsRef, {
|
|
145
|
-
textCredits: newTextCredits,
|
|
146
|
-
imageCredits: newImageCredits,
|
|
147
|
-
purchasedAt,
|
|
148
|
-
lastUpdatedAt: now,
|
|
149
|
-
lastPurchaseAt: now,
|
|
150
|
-
processedPurchases,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return { textCredits: newTextCredits, imageCredits: newImageCredits };
|
|
154
|
-
});
|
|
94
|
+
const result = await initializeCreditsTransaction(
|
|
95
|
+
db,
|
|
96
|
+
creditsRef,
|
|
97
|
+
this.config,
|
|
98
|
+
purchaseId
|
|
99
|
+
);
|
|
155
100
|
|
|
156
101
|
return {
|
|
157
102
|
success: true,
|
|
@@ -166,10 +111,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
166
111
|
return {
|
|
167
112
|
success: false,
|
|
168
113
|
error: {
|
|
169
|
-
message:
|
|
170
|
-
error instanceof Error
|
|
171
|
-
? error.message
|
|
172
|
-
: "Failed to initialize credits",
|
|
114
|
+
message: error instanceof Error ? error.message : "Failed to initialize credits",
|
|
173
115
|
code: "INIT_FAILED",
|
|
174
116
|
},
|
|
175
117
|
};
|
|
@@ -192,62 +134,35 @@ export class CreditsRepository extends BaseRepository {
|
|
|
192
134
|
const creditsRef = this.getCreditsDocRef(db, userId);
|
|
193
135
|
const fieldName = creditType === "text" ? "textCredits" : "imageCredits";
|
|
194
136
|
|
|
195
|
-
const newCredits = await runTransaction(db, async (transaction) => {
|
|
137
|
+
const newCredits = await runTransaction(db, async (transaction: Transaction) => {
|
|
196
138
|
const creditsDoc = await transaction.get(creditsRef);
|
|
197
|
-
|
|
198
|
-
if (!creditsDoc.exists()) {
|
|
199
|
-
throw new Error("NO_CREDITS");
|
|
200
|
-
}
|
|
139
|
+
if (!creditsDoc.exists()) throw new Error("NO_CREDITS");
|
|
201
140
|
|
|
202
141
|
const currentCredits = creditsDoc.data()[fieldName] as number;
|
|
203
|
-
|
|
204
|
-
if (currentCredits <= 0) {
|
|
205
|
-
throw new Error("CREDITS_EXHAUSTED");
|
|
206
|
-
}
|
|
142
|
+
if (currentCredits <= 0) throw new Error("CREDITS_EXHAUSTED");
|
|
207
143
|
|
|
208
144
|
const updatedCredits = currentCredits - 1;
|
|
209
145
|
transaction.update(creditsRef, {
|
|
210
146
|
[fieldName]: updatedCredits,
|
|
211
147
|
lastUpdatedAt: serverTimestamp(),
|
|
212
148
|
});
|
|
213
|
-
|
|
214
149
|
return updatedCredits;
|
|
215
150
|
});
|
|
216
151
|
|
|
217
152
|
return { success: true, remainingCredits: newCredits };
|
|
218
153
|
} catch (error) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (errorMessage === "NO_CREDITS") {
|
|
223
|
-
return {
|
|
224
|
-
success: false,
|
|
225
|
-
error: { message: "No credits found", code: "NO_CREDITS" },
|
|
226
|
-
};
|
|
227
|
-
}
|
|
154
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
155
|
+
const code = msg === "NO_CREDITS" || msg === "CREDITS_EXHAUSTED" ? msg : "DEDUCT_FAILED";
|
|
156
|
+
const message = msg === "NO_CREDITS" ? "No credits found" : msg === "CREDITS_EXHAUSTED" ? "Credits exhausted" : msg;
|
|
228
157
|
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
success: false,
|
|
232
|
-
error: { message: "Credits exhausted", code: "CREDITS_EXHAUSTED" },
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
success: false,
|
|
238
|
-
error: { message: errorMessage, code: "DEDUCT_FAILED" },
|
|
239
|
-
};
|
|
158
|
+
return { success: false, error: { message, code } };
|
|
240
159
|
}
|
|
241
160
|
}
|
|
242
161
|
|
|
243
162
|
async hasCredits(userId: string, creditType: CreditType): Promise<boolean> {
|
|
244
163
|
const result = await this.getCredits(userId);
|
|
245
|
-
if (!result.success || !result.data)
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const credits =
|
|
250
|
-
creditType === "text" ? result.data.textCredits : result.data.imageCredits;
|
|
164
|
+
if (!result.success || !result.data) return false;
|
|
165
|
+
const credits = creditType === "text" ? result.data.textCredits : result.data.imageCredits;
|
|
251
166
|
return credits > 0;
|
|
252
167
|
}
|
|
253
168
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
runTransaction,
|
|
4
|
+
serverTimestamp,
|
|
5
|
+
type Firestore,
|
|
6
|
+
type FieldValue,
|
|
7
|
+
type Transaction,
|
|
8
|
+
type DocumentReference,
|
|
9
|
+
} from "firebase/firestore";
|
|
10
|
+
import type { CreditsConfig } from "../../domain/entities/Credits";
|
|
11
|
+
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
12
|
+
|
|
13
|
+
interface InitializationResult {
|
|
14
|
+
textCredits: number;
|
|
15
|
+
imageCredits: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function initializeCreditsTransaction(
|
|
19
|
+
db: Firestore,
|
|
20
|
+
creditsRef: DocumentReference,
|
|
21
|
+
config: CreditsConfig,
|
|
22
|
+
purchaseId?: string
|
|
23
|
+
): Promise<InitializationResult> {
|
|
24
|
+
return runTransaction(db, async (transaction: Transaction) => {
|
|
25
|
+
const creditsDoc = await transaction.get(creditsRef);
|
|
26
|
+
const now = serverTimestamp();
|
|
27
|
+
|
|
28
|
+
let newTextCredits = config.textCreditLimit;
|
|
29
|
+
let newImageCredits = config.imageCreditLimit;
|
|
30
|
+
let purchasedAt = now;
|
|
31
|
+
let processedPurchases: string[] = [];
|
|
32
|
+
|
|
33
|
+
if (creditsDoc.exists()) {
|
|
34
|
+
const existing = creditsDoc.data() as UserCreditsDocumentRead;
|
|
35
|
+
processedPurchases = existing.processedPurchases || [];
|
|
36
|
+
|
|
37
|
+
if (purchaseId && processedPurchases.includes(purchaseId)) {
|
|
38
|
+
return {
|
|
39
|
+
textCredits: existing.textCredits,
|
|
40
|
+
imageCredits: existing.imageCredits,
|
|
41
|
+
alreadyProcessed: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
newTextCredits = (existing.textCredits || 0) + config.textCreditLimit;
|
|
46
|
+
newImageCredits = (existing.imageCredits || 0) + config.imageCreditLimit;
|
|
47
|
+
if (existing.purchasedAt) {
|
|
48
|
+
purchasedAt = existing.purchasedAt as unknown as FieldValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (purchaseId) {
|
|
53
|
+
processedPurchases = [...processedPurchases, purchaseId].slice(-10);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
transaction.set(creditsRef, {
|
|
57
|
+
textCredits: newTextCredits,
|
|
58
|
+
imageCredits: newImageCredits,
|
|
59
|
+
purchasedAt,
|
|
60
|
+
lastUpdatedAt: now,
|
|
61
|
+
lastPurchaseAt: now,
|
|
62
|
+
processedPurchases,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { textCredits: newTextCredits, imageCredits: newImageCredits };
|
|
66
|
+
});
|
|
67
|
+
}
|