@umituz/react-native-subscription 2.9.5 → 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.5",
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
- interface FirestoreTimestamp {
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
- const result = await runTransaction(db, async (transaction) => {
110
- const creditsDoc = await transaction.get(creditsRef);
111
- const now = serverTimestamp();
112
-
113
- let newTextCredits = this.config.textCreditLimit;
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 errorMessage =
220
- error instanceof Error ? error.message : "Unknown error";
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
- if (errorMessage === "CREDITS_EXHAUSTED") {
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
- return false;
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
+ }